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

View file

@ -1,8 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<!-- Application theme. -->
<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="elevation">0dp</item>
@ -69,8 +70,7 @@
<item name="headerTextColor">@color/text_title</item>
</style>
<style name="MainAppTheme" parent="AppTheme">
</style>
<style name="MainAppTheme" parent="AppTheme"></style>
<style name="MainAppTheme.Dark">
<item name="colorPrimary">@color/brand_400</item>
@ -541,7 +541,7 @@
<style name="SubscriptionListTitle" parent="GemPurchaseListItem">
<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="paddingEnd">32dp</item>
</style>
@ -581,6 +581,7 @@
<style name="Pill.Content" parent="Pill">
<item name="android:background">@drawable/pill_bg_content</item>
</style>
<style name="Pill.Purple">
<item name="android:textColor">@color/white</item>
<item name="android:background">@drawable/pill_bg_purple_300</item>
@ -618,7 +619,7 @@
<style name="subscriptionBoxText.Title">
<item name="android:textSize">14sp</item>
<item name="android:fontFamily" >
<item name="android:fontFamily">
@string/font_family_medium
</item>
<item name="android:textColor">@color/text_primary</item>
@ -627,7 +628,7 @@
<style name="subscriptionBoxText.Subtitle">
<item name="android:textSize">14sp</item>
<item name="android:fontFamily" >
<item name="android:fontFamily">
@string/font_family_regular
</item>
<item name="android:textColor">@color/text_primary</item>
@ -635,7 +636,7 @@
<style name="SubscriptionListDescription" parent="GemPurchaseListItemDescription">
<item name="android:layout_marginBottom">24dp</item>
<item name="android:fontFamily" >
<item name="android:fontFamily">
@string/font_family_condensed
</item>
<item name="android:textSize">14sp</item>
@ -881,12 +882,15 @@
<style name="RedTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/red_10</item>
</style>
<style name="YellowTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/yellow_5</item>
</style>
<style name="BlueTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/blue_10</item>
</style>
<style name="PurpleTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/brand_300</item>
</style>
@ -919,7 +923,6 @@
</style>
<style name="TaskFormTextInputLayoutAppearance" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<!-- reference our hint & error styles -->
<item name="boxBackgroundColor">?colorTintedBackground</item>
@ -1021,6 +1024,7 @@
<item name="android:textSize">12sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="ActiveLabel">
<item name="android:background">@drawable/pill_bg_teal_100</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.UserRepository
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.SoundManager
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.common.habitica.api.HostConfig
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.MainNavigationController
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import io.mockk.clearAllMocks
import io.mockk.every
@ -104,7 +104,10 @@ open class HabiticaTestCase : TestCase() {
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")
return gson.fromJson(gson.newJsonReader(InputStreamReader(userStream)), type)
}
@ -132,7 +135,11 @@ open class HabiticaTestCase : TestCase() {
}
@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) {
it.set(obj, value)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,8 @@ import android.view.View
import androidx.fragment.app.testing.launchFragmentInContainer
import com.habitrpg.android.habitica.R
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.common.habitica.helpers.MainNavigationController
import io.github.kakaocup.kakao.common.views.KView
import io.github.kakaocup.kakao.recycler.KRecyclerItem
import io.github.kakaocup.kakao.recycler.KRecyclerView
@ -30,9 +30,10 @@ class SectionItem(parent: Matcher<View>) : KRecyclerItem<PetItem>(parent) {
}
class StableScreen : Screen<StableScreen>() {
val recycler: KRecyclerView = KRecyclerView({
withId(R.id.recyclerView)
}, itemTypeBuilder = {
val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView)
}, itemTypeBuilder = {
itemType(::SectionItem)
itemType(::PetItem)
})
@ -48,9 +49,10 @@ internal class StableRecyclerFragmentTest : FragmentTestCase<StableRecyclerFragm
}
override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment
}
scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment
}
}
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>() {
val recycler: KRecyclerView = KRecyclerView({
withId(R.id.recyclerView)
}, itemTypeBuilder = {
val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView)
}, itemTypeBuilder = {
itemType(::TaskItem)
})
}
internal class TaskRecyclerViewFragmentTest : FragmentTestCase<TaskRecyclerViewFragment, FragmentRefreshRecyclerviewBinding, TaskListScreen>(false) {
lateinit var tasks: MutableCollection<Task>
override fun makeFragment() {
@ -46,9 +46,10 @@ internal class TaskRecyclerViewFragmentTest : FragmentTestCase<TaskRecyclerViewF
}
override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment
}
scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment
}
}
override val screen = TaskListScreen()

View file

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

View file

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

View file

@ -1,9 +1,10 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.FAQArticle
import kotlinx.coroutines.flow.Flow
interface FAQRepository : BaseRepository {
fun getArticles(): Flow<List<FAQArticle>>
fun getArticle(position: Int): Flow<FAQArticle>
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.FAQArticle
import kotlinx.coroutines.flow.Flow
interface FAQRepository : BaseRepository {
fun getArticles(): Flow<List<FAQArticle>>
fun getArticle(position: Int): Flow<FAQArticle>
}

View file

@ -1,95 +1,163 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import kotlinx.coroutines.flow.Flow
interface InventoryRepository : BaseRepository {
fun getArmoireRemainingCount(): Flow<Int>
fun getInAppRewards(): Flow<List<ShopItem>>
fun getInAppReward(key: String): Flow<ShopItem>
fun getOwnedEquipment(): Flow<List<Equipment>>
fun getMounts(): Flow<List<Mount>>
fun getOwnedMounts(): Flow<List<OwnedMount>>
fun getPets(): Flow<List<Pet>>
fun getOwnedPets(): Flow<List<OwnedPet>>
fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
suspend fun retrieveInAppRewards(): List<ShopItem>?
fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getEquipmentType(type: String, set: String): Flow<List<Equipment>>
fun getOwnedItems(itemType: String, includeZero: Boolean = false): Flow<List<OwnedItem>>
fun getOwnedItems(includeZero: Boolean = false): Flow<Map<String, OwnedItem>>
fun getEquipment(key: String): Flow<Equipment>
suspend fun openMysteryItem(user: User?): Equipment?
fun saveEquipment(equipment: Equipment)
fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>>
fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>>
fun updateOwnedEquipment(user: User)
suspend fun changeOwnedCount(type: String, key: String, amountToAdd: Int)
suspend fun sellItem(type: String, key: String): User?
suspend fun sellItem(item: OwnedItem): User?
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>>
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import kotlinx.coroutines.flow.Flow
interface InventoryRepository : BaseRepository {
fun getArmoireRemainingCount(): Flow<Int>
fun getInAppRewards(): Flow<List<ShopItem>>
fun getInAppReward(key: String): Flow<ShopItem>
fun getOwnedEquipment(): Flow<List<Equipment>>
fun getMounts(): Flow<List<Mount>>
fun getOwnedMounts(): Flow<List<OwnedMount>>
fun getPets(): Flow<List<Pet>>
fun getOwnedPets(): Flow<List<OwnedPet>>
fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
suspend fun retrieveInAppRewards(): List<ShopItem>?
fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getEquipmentType(
type: String,
set: String,
): Flow<List<Equipment>>
fun getOwnedItems(
itemType: String,
includeZero: Boolean = false,
): Flow<List<OwnedItem>>
fun getOwnedItems(includeZero: Boolean = false): Flow<Map<String, OwnedItem>>
fun getEquipment(key: String): Flow<Equipment>
suspend fun openMysteryItem(user: User?): Equipment?
fun saveEquipment(equipment: Equipment)
fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>>
fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>>
fun updateOwnedEquipment(user: User)
suspend fun changeOwnedCount(
type: String,
key: String,
amountToAdd: Int,
)
suspend fun sellItem(
type: String,
key: String,
): User?
suspend fun sellItem(item: OwnedItem): User?
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
import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User
interface SetupCustomizationRepository {
fun getCustomizations(type: String, user: User): List<SetupCustomization>
fun getCustomizations(type: String, subtype: String?, user: User): List<SetupCustomization>
companion object {
const val CATEGORY_BODY = "body"
const val CATEGORY_SKIN = "skin"
const val CATEGORY_HAIR = "hair"
const val CATEGORY_EXTRAS = "extras"
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"
}
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User
interface SetupCustomizationRepository {
fun getCustomizations(
type: String,
user: User,
): List<SetupCustomization>
fun getCustomizations(
type: String,
subtype: String?,
user: User,
): List<SetupCustomization>
companion object {
const val CATEGORY_BODY = "body"
const val CATEGORY_SKIN = "skin"
const val CATEGORY_HAIR = "hair"
const val CATEGORY_EXTRAS = "extras"
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
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.FindUsernameResult
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface SocialRepository : BaseRepository {
fun getUserGroups(type: String?): Flow<List<Group>>
suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>?
fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
suspend fun markMessagesSeen(seenGroupId: String)
suspend fun flagMessage(
chatMessageID: String,
additionalInfo: String,
groupID: String? = null
): Void?
suspend fun reportMember(memberID: String, data: Map<String, String>): Void?
suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage?
suspend fun deleteMessage(chatMessage: ChatMessage): Void?
suspend fun postGroupChat(
groupId: String,
messageObject: HashMap<String, String>
): PostChatMessageResult?
suspend fun postGroupChat(groupId: String, message: String): PostChatMessageResult?
suspend fun retrieveGroup(id: String): Group?
fun getGroup(id: String?): Flow<Group?>
suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group?
suspend fun joinGroup(id: String?): Group?
suspend fun createGroup(
name: String?,
description: String?,
leader: String?,
type: String?,
privacy: String?,
leaderCreateChallenge: Boolean?
): Group?
suspend fun updateGroup(
group: Group?,
name: String?,
description: String?,
leader: String?,
leaderCreateChallenge: Boolean?
): Group?
fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>>
suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>?
suspend fun retrieveInboxConversations(): List<InboxConversation>?
fun getInboxConversations(): Flow<RealmResults<InboxConversation>>
suspend fun postPrivateMessage(
recipientId: String,
messageObject: HashMap<String, String>
): List<ChatMessage>?
suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>?
suspend fun getPartyMembers(id: String): Flow<List<Member>>
suspend fun getGroupMembers(id: String): Flow<List<Member>>
suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>?
suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>): List<InviteResponse>?
suspend fun retrieveMember(userId: String?, fromHall: Boolean = false): Member?
suspend fun findUsernames(
username: String,
context: String? = null,
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>?
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.FindUsernameResult
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface SocialRepository : BaseRepository {
fun getUserGroups(type: String?): Flow<List<Group>>
suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>?
fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
suspend fun markMessagesSeen(seenGroupId: String)
suspend fun flagMessage(
chatMessageID: String,
additionalInfo: String,
groupID: String? = null,
): Void?
suspend fun reportMember(
memberID: String,
data: Map<String, String>,
): Void?
suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage?
suspend fun deleteMessage(chatMessage: ChatMessage): Void?
suspend fun postGroupChat(
groupId: String,
messageObject: HashMap<String, String>,
): PostChatMessageResult?
suspend fun postGroupChat(
groupId: String,
message: String,
): PostChatMessageResult?
suspend fun retrieveGroup(id: String): Group?
fun getGroup(id: String?): Flow<Group?>
suspend fun leaveGroup(
id: String?,
keepChallenges: Boolean,
): Group?
suspend fun joinGroup(id: String?): Group?
suspend fun createGroup(
name: String?,
description: String?,
leader: String?,
type: String?,
privacy: String?,
leaderCreateChallenge: Boolean?,
): Group?
suspend fun updateGroup(
group: Group?,
name: String?,
description: String?,
leader: String?,
leaderCreateChallenge: Boolean?,
): Group?
fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>>
suspend fun retrieveInboxMessages(
uuid: String,
page: Int,
): List<ChatMessage>?
suspend fun retrieveInboxConversations(): List<InboxConversation>?
fun getInboxConversations(): Flow<RealmResults<InboxConversation>>
suspend fun postPrivateMessage(
recipientId: String,
messageObject: HashMap<String, String>,
): List<ChatMessage>?
suspend fun postPrivateMessage(
recipientId: String,
message: String,
): List<ChatMessage>?
suspend fun getPartyMembers(id: String): Flow<List<Member>>
suspend fun getGroupMembers(id: String): Flow<List<Member>>
suspend fun retrievePartyMembers(
id: String,
includeAllPublicFields: Boolean,
): List<Member>?
suspend fun inviteToGroup(
id: String,
inviteData: Map<String, Any>,
): List<InviteResponse>?
suspend fun retrieveMember(
userId: String?,
fromHall: Boolean = false,
): Member?
suspend fun findUsernames(
username: String,
context: String? = null,
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
import com.habitrpg.android.habitica.models.Tag
import kotlinx.coroutines.flow.Flow
interface TagRepository : BaseRepository {
fun getTags(): Flow<List<Tag>>
fun getTags(userId: String): Flow<List<Tag>>
suspend fun createTag(tag: Tag): Tag?
suspend fun updateTag(tag: Tag): Tag?
suspend fun deleteTag(id: String): 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>
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Tag
import kotlinx.coroutines.flow.Flow
interface TagRepository : BaseRepository {
fun getTags(): Flow<List<Tag>>
fun getTags(userId: String): Flow<List<Tag>>
suspend fun createTag(tag: Tag): Tag?
suspend fun updateTag(tag: Tag): Tag?
suspend fun deleteTag(id: String): 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
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.flow.Flow
import java.util.Date
interface TaskRepository : BaseRepository {
fun getTasks(taskType: TaskType, userID: String? = null, includedGroupIDs: Array<String>): Flow<List<Task>>
fun saveTasks(userId: String, order: TasksOrder, tasks: TaskList)
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList?
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): TaskList?
suspend fun taskChecked(
user: User?,
task: Task,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
): TaskScoringResult?
suspend fun taskChecked(
user: User?,
taskId: String,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
): TaskScoringResult?
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task>
suspend fun createTask(task: Task, force: Boolean = false): Task?
suspend fun updateTask(task: Task, force: Boolean = false): Task?
suspend fun 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)
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.flow.Flow
import java.util.Date
interface TaskRepository : BaseRepository {
fun getTasks(
taskType: TaskType,
userID: String? = null,
includedGroupIDs: Array<String>,
): Flow<List<Task>>
fun saveTasks(
userId: String,
order: TasksOrder,
tasks: TaskList,
)
suspend fun retrieveTasks(
userId: String,
tasksOrder: TasksOrder,
): TaskList?
suspend fun retrieveTasks(
userId: String,
tasksOrder: TasksOrder,
dueDate: Date,
): TaskList?
suspend fun taskChecked(
user: User?,
task: Task,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult?
suspend fun taskChecked(
user: User?,
taskId: String,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult?
suspend fun scoreChecklistItem(
taskId: String,
itemId: String,
): Task?
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task>
suspend fun createTask(
task: Task,
force: Boolean = false,
): Task?
suspend fun updateTask(
task: Task,
force: Boolean = false,
): Task?
suspend fun 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
import com.habitrpg.android.habitica.models.TutorialStep
import kotlinx.coroutines.flow.Flow
interface TutorialRepository : BaseRepository {
fun getTutorialStep(key: String): Flow<TutorialStep>
fun getTutorialSteps(keys: List<String>): Flow<out List<TutorialStep>>
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.TutorialStep
import kotlinx.coroutines.flow.Flow
interface TutorialRepository : BaseRepository {
fun getTutorialStep(key: String): Flow<TutorialStep>
fun getTutorialSteps(keys: List<String>): Flow<out List<TutorialStep>>
}

View file

@ -1,89 +1,147 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.common.habitica.models.Notification
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import com.habitrpg.shared.habitica.models.tasks.Attribute
import kotlinx.coroutines.flow.Flow
interface UserRepository : BaseRepository {
fun getUser(): 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 retrieveUser(withTasks: Boolean = false, forced: Boolean = false, overrideExisting: Boolean = false): User?
suspend fun revive(): Equipment?
suspend fun resetTutorial(): User?
suspend fun sleep(user: User): User?
fun getSkills(user: User): Flow<List<Skill>>
fun getSpecialItems(user: User): Flow<List<Skill>>
suspend fun useSkill(key: String, target: String?, taskId: String): SkillResponse?
suspend fun useSkill(key: String, target: String?): SkillResponse?
suspend fun disableClasses(): User?
suspend fun changeClass(selectedClass: String? = null): User?
suspend fun unlockPath(path: String, price: Int): UnlockResponse?
suspend fun unlockPath(customization: Customization): UnlockResponse?
suspend fun runCron(tasks: MutableList<Task>)
suspend fun runCron()
suspend fun getNews(): List<Any>?
suspend fun getNewsNotification(): Notification?
suspend fun readNotification(id: String): List<Any>?
suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun changeCustomDayStart(dayStartTime: Int): User?
suspend fun updateLanguage(languageCode: String): 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?
}
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.common.habitica.models.Notification
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import com.habitrpg.shared.habitica.models.tasks.Attribute
import kotlinx.coroutines.flow.Flow
interface UserRepository : BaseRepository {
fun getUser(): 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 retrieveUser(
withTasks: Boolean = false,
forced: Boolean = false,
overrideExisting: Boolean = false,
): User?
suspend fun revive(): Equipment?
suspend fun resetTutorial(): User?
suspend fun sleep(user: User): User?
fun getSkills(user: User): Flow<List<Skill>>
fun getSpecialItems(user: User): Flow<List<Skill>>
suspend fun useSkill(
key: String,
target: String?,
taskId: String,
): SkillResponse?
suspend fun useSkill(
key: String,
target: String?,
): SkillResponse?
suspend fun disableClasses(): User?
suspend fun changeClass(selectedClass: String? = null): User?
suspend fun unlockPath(
path: String,
price: Int,
): UnlockResponse?
suspend fun unlockPath(customization: Customization): UnlockResponse?
suspend fun runCron(tasks: MutableList<Task>)
suspend fun runCron()
suspend fun getNews(): List<Any>?
suspend fun getNewsNotification(): Notification?
suspend fun readNotification(id: String): List<Any>?
suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun changeCustomDayStart(dayStartTime: Int): User?
suspend fun updateLanguage(languageCode: String): 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,
override val hostConfig: HostConfig,
private val notificationsManager: NotificationsManager,
private val context: Context
private val context: Context,
) : ApiClient {
private lateinit var retrofitAdapter: Retrofit
// I think we don't need the ApiClientImpl anymore we could just use ApiService
@ -114,72 +113,78 @@ class ApiClientImpl(
val calendar = GregorianCalendar()
val timeZone = calendar.timeZone
val timezoneOffset = -TimeUnit.MINUTES.convert(
timeZone.getOffset(calendar.timeInMillis).toLong(),
TimeUnit.MILLISECONDS
)
val timezoneOffset =
-TimeUnit.MINUTES.convert(
timeZone.getOffset(calendar.timeInMillis).toLong(),
TimeUnit.MILLISECONDS,
)
val cacheSize: Long = 10 * 1024 * 1024 // 10 MB
val cache = Cache(File(context.cacheDir, "http_cache"), cacheSize)
val client = OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor { chain ->
val original = chain.request()
var builder: Request.Builder = original.newBuilder()
if (this.hostConfig.hasAuthentication()) {
builder = builder
.header("x-api-key", this.hostConfig.apiKey)
.header("x-api-user", this.hostConfig.userID)
}
builder = builder.header("x-client", "habitica-android")
.header("x-user-timezoneOffset", timezoneOffset.toString())
if (userAgent != null) {
builder = builder.header("user-agent", userAgent)
}
if (BuildConfig.STAGING_KEY.isNotEmpty()) {
builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY)
}
val request = builder.method(original.method, original.body)
.build()
lastAPICallURL = original.url.toString()
val response = chain.proceed(request)
if (response.isSuccessful) {
hideConnectionProblemDialog()
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 {
val client =
OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor { chain ->
val original = chain.request()
var builder: Request.Builder = original.newBuilder()
if (this.hostConfig.hasAuthentication()) {
builder =
builder
.header("x-api-key", this.hostConfig.apiKey)
.header("x-api-user", this.hostConfig.userID)
}
builder =
builder.header("x-client", "habitica-android")
.header("x-user-timezoneOffset", timezoneOffset.toString())
if (userAgent != null) {
builder = builder.header("user-agent", userAgent)
}
if (BuildConfig.STAGING_KEY.isNotEmpty()) {
builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY)
}
val request =
builder.method(original.method, original.body)
.build()
lastAPICallURL = original.url.toString()
val response = chain.proceed(request)
if (response.isSuccessful) {
hideConnectionProblemDialog()
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)
.readTimeout(2400, TimeUnit.SECONDS)
.build()
.addInterceptor(logging)
.readTimeout(2400, TimeUnit.SECONDS)
.build()
val server = Server(this.hostConfig.address)
retrofitAdapter = Retrofit.Builder()
.client(client)
.baseUrl(server.toString())
.addConverterFactory(converter)
.build()
retrofitAdapter =
Retrofit.Builder()
.client(client)
.baseUrl(server.toString())
.addConverterFactory(converter)
.build()
this.apiService = retrofitAdapter.create(ApiService::class.java)
}
@ -195,7 +200,7 @@ class ApiClientImpl(
username: String,
email: String,
password: String,
confirmPassword: String
confirmPassword: String,
): UserAuthResponse? {
val auth = UserAuth()
auth.username = username
@ -205,7 +210,10 @@ class ApiClientImpl(
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()
auth.username = username
auth.password = password
@ -215,7 +223,7 @@ class ApiClientImpl(
override suspend fun connectSocial(
network: String,
userId: String,
accessToken: String
accessToken: String,
): UserAuthResponse? {
val auth = UserAuthSocial()
auth.network = network
@ -243,14 +251,14 @@ class ApiClientImpl(
var isUserInputCall = false
@Suppress("DEPRECATION")
if (SocketException::class.java.isAssignableFrom(throwableClass)
|| SSLException::class.java.isAssignableFrom(throwableClass)
if (SocketException::class.java.isAssignableFrom(throwableClass) ||
SSLException::class.java.isAssignableFrom(throwableClass)
) {
this.showConnectionProblemDialog(R.string.internal_error_api, isUserInputCall)
} else if (throwableClass == SocketTimeoutException::class.java || UnknownHostException::class.java == throwableClass || IOException::class.java == throwableClass) {
this.showConnectionProblemDialog(
R.string.network_error_no_network_body,
isUserInputCall
isUserInputCall,
)
} else if (HttpException::class.java.isAssignableFrom(throwable.javaClass)) {
val error = throwable as HttpException
@ -258,10 +266,11 @@ class ApiClientImpl(
val status = error.code()
val requestUrl = error.response()?.raw()?.request?.url
val path = requestUrl?.encodedPath?.removePrefix("/api/v4") ?: ""
isUserInputCall = when {
path.startsWith("/groups") && path.endsWith("invite") -> true
else -> false
}
isUserInputCall =
when {
path.startsWith("/groups") && path.endsWith("invite") -> true
else -> false
}
if (res.message != null && res.message == "RECEIPT_ALREADY_USED") {
return
@ -278,7 +287,7 @@ class ApiClientImpl(
showConnectionProblemDialog(
R.string.authentication_error_title,
R.string.authentication_error_body,
isUserInputCall
isUserInputCall,
)
}
} else if (status in 500..599) {
@ -295,15 +304,16 @@ class ApiClientImpl(
override suspend fun updateMember(
memberID: String,
updateData: Map<String, Map<String, Boolean>>
updateData: Map<String, Map<String, Boolean>>,
): Member? {
return process { apiService.updateUser(memberID, updateData) }
}
override fun getErrorResponse(throwable: HttpException): ErrorResponse {
val errorResponse = throwable.response()?.errorBody() ?: return ErrorResponse()
val errorConverter = converter
.responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter)
val errorConverter =
converter
.responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter)
return try {
errorConverter?.convert(errorResponse) as ErrorResponse
} catch (e: IOException) {
@ -319,7 +329,10 @@ class ApiClientImpl(
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) }
}
@ -333,7 +346,7 @@ class ApiClientImpl(
private fun showConnectionProblemDialog(
resourceMessageString: Int,
isFromUserInput: Boolean
isFromUserInput: Boolean,
) {
showConnectionProblemDialog(null, context.getString(resourceMessageString), isFromUserInput)
}
@ -341,38 +354,41 @@ class ApiClientImpl(
private fun showConnectionProblemDialog(
resourceTitleString: Int,
resourceMessageString: Int,
isFromUserInput: Boolean
isFromUserInput: Boolean,
) {
showConnectionProblemDialog(
context.getString(resourceTitleString),
context.getString(resourceMessageString),
isFromUserInput
isFromUserInput,
)
}
private var erroredRequestCount = 0
private fun showConnectionProblemDialog(
resourceTitleString: String?,
resourceMessageString: String,
isFromUserInput: Boolean
isFromUserInput: Boolean,
) {
erroredRequestCount += 1
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication)
val application =
(context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication)
application?.currentActivity?.get()
?.showConnectionProblem(
erroredRequestCount,
resourceTitleString,
resourceMessageString,
isFromUserInput
isFromUserInput,
)
}
private fun hideConnectionProblemDialog() {
if (erroredRequestCount == 0) return
erroredRequestCount = 0
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication)
val application =
(context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication)
application?.currentActivity?.get()
?.hideConnectionProblem()
}
@ -382,7 +398,10 @@ class ApiClientImpl(
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.apiKey = apiToken ?: ""
Analytics.setUserID(hostConfig.userID)
@ -391,9 +410,10 @@ class ApiClientImpl(
override suspend fun getStatus(): Status? = process { apiService.getStatus() }
override suspend fun syncUserStats(): User? = process { apiService.syncUserStats() }
override suspend fun reportChallenge(
challengeid: String,
updateData: Map<String, String>
updateData: Map<String, String>,
): Void? {
return process { apiService.reportChallenge(challengeid, updateData) }
}
@ -414,15 +434,24 @@ class ApiClientImpl(
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) }
}
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))) }
}
override suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void? {
override suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void? {
return process { apiService.unlinkAllTasks(challengeID, keepOption) }
}
@ -430,17 +459,22 @@ class ApiClientImpl(
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 {
apiService.purchaseItem(
type,
itemKey,
mapOf(Pair("quantity", purchaseQuantity))
mapOf(Pair("quantity", purchaseQuantity)),
)
}
}
val lastSubscribeCall: Date? = null
override suspend fun validateSubscription(request: PurchaseValidationRequest): Any? {
return if (lastSubscribeCall == null || Date().time - lastSubscribeCall.time > 60000) {
process { apiService.validateSubscription(request) }
@ -461,7 +495,10 @@ class ApiClientImpl(
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) }
}
@ -477,17 +514,26 @@ class ApiClientImpl(
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) }
}
override suspend fun feedPet(petKey: String, foodKey: String): FeedResponse? {
override suspend fun feedPet(
petKey: String,
foodKey: String,
): FeedResponse? {
val response = apiService.feedPet(petKey, foodKey)
response.data?.message = response.message
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) }
}
@ -497,7 +543,10 @@ class ApiClientImpl(
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) }
}
@ -513,7 +562,10 @@ class ApiClientImpl(
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) }
}
@ -521,11 +573,17 @@ class ApiClientImpl(
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) }
}
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? {
override suspend fun scoreChecklistItem(
taskId: String,
itemId: String,
): Task? {
return process { apiService.scoreChecklistItem(taskId, itemId) }
}
@ -533,7 +591,10 @@ class ApiClientImpl(
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) }
}
@ -541,7 +602,10 @@ class ApiClientImpl(
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) }
}
@ -553,7 +617,10 @@ class ApiClientImpl(
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) }
}
@ -568,12 +635,15 @@ class ApiClientImpl(
override suspend fun useSkill(
skillName: String,
targetType: String,
targetId: String
targetId: String,
): SkillResponse? {
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) }
}
@ -605,11 +675,17 @@ class ApiClientImpl(
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))
}
override suspend fun removeMemberFromGroup(groupID: String, userID: String): Void? {
override suspend fun removeMemberFromGroup(
groupID: String,
userID: String,
): Void? {
return processResponse(apiService.removeMemberFromGroup(groupID, userID))
}
@ -621,18 +697,24 @@ class ApiClientImpl(
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))
}
override suspend fun postGroupChat(
groupId: String,
message: Map<String, String>
message: Map<String, String>,
): PostChatMessageResult? {
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) }
}
@ -642,7 +724,7 @@ class ApiClientImpl(
override suspend fun getGroupMembers(
groupId: String,
includeAllPublicFields: Boolean?
includeAllPublicFields: Boolean?,
): List<Member>? {
return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields))
}
@ -650,28 +732,37 @@ class ApiClientImpl(
override suspend fun getGroupMembers(
groupId: String,
includeAllPublicFields: Boolean?,
lastId: String
lastId: String,
): List<Member>? {
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) }
}
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) }
}
override suspend fun flagMessage(
groupId: String,
mid: String,
data: MutableMap<String, String>
data: MutableMap<String, String>,
): Void? {
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) }
}
@ -681,7 +772,7 @@ class ApiClientImpl(
override suspend fun inviteToGroup(
groupId: String,
inviteData: Map<String, Any>
inviteData: Map<String, Any>,
): List<InviteResponse>? {
return process { apiService.inviteToGroup(groupId, inviteData) }
}
@ -692,7 +783,7 @@ class ApiClientImpl(
override suspend fun getGroupInvites(
groupId: String,
includeAllPublicFields: Boolean?
includeAllPublicFields: Boolean?,
): List<Member>? {
return process { apiService.getGroupInvites(groupId, includeAllPublicFields) }
}
@ -709,11 +800,17 @@ class ApiClientImpl(
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) }
}
override suspend fun inviteToQuest(groupId: String, questKey: String): Quest? {
override suspend fun inviteToQuest(
groupId: String,
questKey: String,
): Quest? {
return process { apiService.inviteToQuest(groupId, questKey) }
}
@ -726,6 +823,7 @@ class ApiClientImpl(
}
private val lastPurchaseValidation: Date? = null
override suspend fun validatePurchase(request: PurchaseValidationRequest): PurchaseValidationResult? {
// make sure a purchase attempt doesn't happen
return if (lastPurchaseValidation == null || Date().time - lastPurchaseValidation.time > 5000) {
@ -739,7 +837,10 @@ class ApiClientImpl(
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) }
}
@ -760,7 +861,7 @@ class ApiClientImpl(
override suspend fun findUsernames(
username: String,
context: String?,
id: String?
id: String?,
): List<FindUsernameResult>? {
return process { apiService.findUsernames(username, context, id) }
}
@ -781,7 +882,10 @@ class ApiClientImpl(
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) {
process { apiService.getUserChallenges(page, memberOnly) }
} else {
@ -801,7 +905,10 @@ class ApiClientImpl(
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) }
}
@ -809,11 +916,17 @@ class ApiClientImpl(
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) }
}
override suspend fun createChallengeTask(challengeId: String, task: Task): Task? {
override suspend fun createChallengeTask(
challengeId: String,
task: Task,
): Task? {
return process { apiService.createChallengeTask(challengeId, task) }
}
@ -867,7 +980,10 @@ class ApiClientImpl(
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) }
}
@ -877,7 +993,10 @@ class ApiClientImpl(
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>()
updateObject["username"] = newLoginName
updateObject["password"] = password
@ -896,7 +1015,10 @@ class ApiClientImpl(
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>()
updateObject["newEmail"] = newEmail
if (password.isNotBlank()) {
@ -908,7 +1030,7 @@ class ApiClientImpl(
override suspend fun updatePassword(
oldPassword: String,
newPassword: String,
newPasswordConfirmation: String
newPasswordConfirmation: String,
): Void? {
val updateObject = HashMap<String, String>()
updateObject["password"] = oldPassword
@ -921,13 +1043,16 @@ class ApiClientImpl(
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 {
apiService.transferGems(
mapOf(
Pair("toUserId", giftedID),
Pair("gemAmount", amount)
)
Pair("gemAmount", amount),
),
)
}
}
@ -940,11 +1065,17 @@ class ApiClientImpl(
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) }
}
override suspend fun unassignFromTask(taskId: String, userID: String): Task? {
override suspend fun unassignFromTask(
taskId: String,
userID: String,
): Task? {
return process { apiService.unassignFromTask(taskId, userID) }
}
@ -952,7 +1083,7 @@ class ApiClientImpl(
strength: Int,
intelligence: Int,
constitution: Int,
perception: Int
perception: Int,
): Stats? {
val body = HashMap<String, Map<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>(
protected val localRepository: T,
protected val apiClient: ApiClient,
protected val authenticationHandler: AuthenticationHandler
protected val authenticationHandler: AuthenticationHandler,
) : BaseRepository {
val currentUserID: String
get() = authenticationHandler.currentUserID ?: ""

View file

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

View file

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

View file

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

View file

@ -1,318 +1,391 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.local.InventoryLocalRepository
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
class InventoryRepositoryImpl(
localRepository: InventoryLocalRepository,
apiClient: ApiClient,
authenticationHandler: AuthenticationHandler,
var appConfigManager: AppConfigManager
) : BaseRepositoryImpl<InventoryLocalRepository>(localRepository, apiClient, authenticationHandler), InventoryRepository {
override fun getQuestContent(keys: List<String>) = localRepository.getQuestContent(keys)
override fun getQuestContent(key: String) = localRepository.getQuestContent(key)
override fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> {
return localRepository.getEquipment(searchedKeys)
}
override fun getArmoireRemainingCount(): Flow<Int> {
return localRepository.getArmoireRemainingCount()
}
override fun getInAppRewards(): Flow<List<ShopItem>> {
return localRepository.getInAppRewards()
}
override fun getInAppReward(key: String): Flow<ShopItem> {
return localRepository.getInAppReward(key)
}
override suspend fun retrieveInAppRewards(): List<ShopItem>? {
val rewards = apiClient.retrieveInAppRewards()
if (rewards != null) {
localRepository.saveInAppRewards(rewards)
}
return rewards
}
override fun getOwnedEquipment(type: String): Flow<List<Equipment>> {
return localRepository.getOwnedEquipment(type)
}
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 getOwnedItems(itemType: String, includeZero: Boolean): Flow<List<OwnedItem>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedItems(itemType, it, includeZero) }
}
override fun getOwnedItems(includeZero: Boolean): Flow<Map<String, OwnedItem>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedItems(it, includeZero) }
}
override fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>> {
return localRepository.getItems(itemClass, keys)
}
override fun getItems(itemClass: Class<out Item>): Flow<List<Item>> {
return localRepository.getItems(itemClass)
}
override fun getEquipment(key: String): Flow<Equipment> {
return localRepository.getEquipment(key)
}
override suspend fun openMysteryItem(user: User?): Equipment? {
val item = apiClient.openMysteryItem()
val equipment = localRepository.getEquipment(item?.key ?: "").firstOrNull() ?: return null
val liveEquipment = localRepository.getLiveObject(equipment)
localRepository.executeTransaction {
liveEquipment?.owned = true
}
localRepository.decrementMysteryItemCount(user)
return equipment
}
override fun saveEquipment(equipment: Equipment) {
localRepository.save(equipment)
}
override fun getMounts(): Flow<List<Mount>> {
return localRepository.getMounts()
}
override fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> {
return localRepository.getMounts(type, group, color)
}
override fun getOwnedMounts(): Flow<List<OwnedMount>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedMounts(it) }
}
override fun getPets(): Flow<List<Pet>> {
return localRepository.getPets()
}
override fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> {
return localRepository.getPets(type, group, color)
}
override fun getOwnedPets(): Flow<List<OwnedPet>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedPets(it) }
}
override fun updateOwnedEquipment(user: User) {
localRepository.updateOwnedEquipment(user)
}
override suspend fun changeOwnedCount(type: String, key: String, amountToAdd: Int) {
localRepository.changeOwnedCount(type, key, currentUserID, amountToAdd)
}
override suspend fun sellItem(type: String, key: String): User? {
val item = localRepository.getOwnedItem(currentUserID, type, key, true).firstOrNull() ?: return null
return sellItem(item)
}
override suspend fun sellItem(item: OwnedItem): User? {
val itemData = localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull() ?: return null
return sellItem(itemData, item)
}
override fun getLatestMysteryItem(): Flow<Equipment> {
return localRepository.getLatestMysteryItem()
}
override fun getItem(type: String, key: String): Flow<Item> {
return localRepository.getItem(type, key)
}
private suspend fun sellItem(item: Item, ownedItem: OwnedItem): User? {
localRepository.executeTransaction {
val liveItem = localRepository.getLiveObject(ownedItem)
liveItem?.numberOwned = (liveItem?.numberOwned ?: 0) - 1
}
val user = apiClient.sellItem(item.type, item.key) ?: return null
return localRepository.soldItem(currentUserID, user)
}
override suspend fun equipGear(equipment: String, asCostume: Boolean): Items? {
return equip(if (asCostume) "costume" else "equipped", equipment)
}
override suspend fun equip(type: String, key: String): Items? {
val liveUser = localRepository.getLiveUser(currentUserID)
if (liveUser != null) {
localRepository.modify(liveUser) { user ->
if (type == "mount") {
user.items?.currentMount = key
} else if (type == "pet") {
user.items?.currentPet = key
}
val outfit = if (type == "costume") {
user.items?.gear?.costume
} else {
user.items?.gear?.equipped
}
when (key.split("_").firstOrNull()) {
"weapon" -> outfit?.weapon = key
"armor" -> outfit?.armor = key
"shield" -> outfit?.shield = key
"eyewear" -> outfit?.eyeWear = key
"head" -> outfit?.head = key
"back" -> outfit?.back = key
"headAccessory" -> outfit?.headAccessory = key
"body" -> outfit?.body = key
}
}
}
val items = apiClient.equipItem(type, key) ?: return null
if (liveUser == null) return null
localRepository.modify(liveUser) { liveUser ->
val newEquipped = items.gear?.equipped
val oldEquipped = liveUser.items?.gear?.equipped
val newCostume = items.gear?.costume
val oldCostume = liveUser.items?.gear?.costume
newEquipped?.let { equipped -> oldEquipped?.updateWith(equipped) }
newCostume?.let { costume -> oldCostume?.updateWith(costume) }
liveUser.items?.currentMount = items.currentMount
liveUser.items?.currentPet = items.currentPet
liveUser.balance = liveUser.balance
}
return items
}
override suspend fun feedPet(pet: Pet, food: Food): FeedResponse? {
val feedResponse = apiClient.feedPet(pet.key, food.key) ?: return null
localRepository.feedPet(food.key, pet.key, feedResponse.value ?: 0, currentUserID)
return feedResponse
}
override suspend fun hatchPet(egg: Egg, hatchingPotion: HatchingPotion, successFunction: () -> Unit): Items? {
if (appConfigManager.enableLocalChanges()) {
localRepository.hatchPet(egg.key, hatchingPotion.key, currentUserID)
successFunction()
}
val items = apiClient.hatchPet(egg.key, hatchingPotion.key) ?: return null
localRepository.save(items, currentUserID)
if (!appConfigManager.enableLocalChanges()) {
successFunction()
}
return items
}
override suspend fun inviteToQuest(quest: QuestContent): Quest? {
val newQuest = apiClient.inviteToQuest("party", quest.key)
localRepository.changeOwnedCount("quests", quest.key, currentUserID, -1)
return newQuest
}
override suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse? {
val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null
val foundUser = user ?: localRepository.getLiveUser(currentUserID) ?: return buyResponse
val copiedUser = localRepository.getUnmanagedCopy(foundUser)
if (buyResponse.items != null) {
copiedUser.items = buyResponse.items
}
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()
}
}
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.local.InventoryLocalRepository
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
class InventoryRepositoryImpl(
localRepository: InventoryLocalRepository,
apiClient: ApiClient,
authenticationHandler: AuthenticationHandler,
var appConfigManager: AppConfigManager,
) : BaseRepositoryImpl<InventoryLocalRepository>(localRepository, apiClient, authenticationHandler),
InventoryRepository {
override fun getQuestContent(keys: List<String>) = localRepository.getQuestContent(keys)
override fun getQuestContent(key: String) = localRepository.getQuestContent(key)
override fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> {
return localRepository.getEquipment(searchedKeys)
}
override fun getArmoireRemainingCount(): Flow<Int> {
return localRepository.getArmoireRemainingCount()
}
override fun getInAppRewards(): Flow<List<ShopItem>> {
return localRepository.getInAppRewards()
}
override fun getInAppReward(key: String): Flow<ShopItem> {
return localRepository.getInAppReward(key)
}
override suspend fun retrieveInAppRewards(): List<ShopItem>? {
val rewards = apiClient.retrieveInAppRewards()
if (rewards != null) {
localRepository.saveInAppRewards(rewards)
}
return rewards
}
override fun getOwnedEquipment(type: String): Flow<List<Equipment>> {
return localRepository.getOwnedEquipment(type)
}
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 getOwnedItems(
itemType: String,
includeZero: Boolean,
): Flow<List<OwnedItem>> {
return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getOwnedItems(
itemType,
it,
includeZero,
)
}
}
override fun getOwnedItems(includeZero: Boolean): Flow<Map<String, OwnedItem>> {
return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getOwnedItems(
it,
includeZero,
)
}
}
override fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>> {
return localRepository.getItems(itemClass, keys)
}
override fun getItems(itemClass: Class<out Item>): Flow<List<Item>> {
return localRepository.getItems(itemClass)
}
override fun getEquipment(key: String): Flow<Equipment> {
return localRepository.getEquipment(key)
}
override suspend fun openMysteryItem(user: User?): Equipment? {
val item = apiClient.openMysteryItem()
val equipment = localRepository.getEquipment(item?.key ?: "").firstOrNull() ?: return null
val liveEquipment = localRepository.getLiveObject(equipment)
localRepository.executeTransaction {
liveEquipment?.owned = true
}
localRepository.decrementMysteryItemCount(user)
return equipment
}
override fun saveEquipment(equipment: Equipment) {
localRepository.save(equipment)
}
override fun getMounts(): Flow<List<Mount>> {
return localRepository.getMounts()
}
override fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>> {
return localRepository.getMounts(type, group, color)
}
override fun getOwnedMounts(): Flow<List<OwnedMount>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedMounts(it) }
}
override fun getPets(): Flow<List<Pet>> {
return localRepository.getPets()
}
override fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>> {
return localRepository.getPets(type, group, color)
}
override fun getOwnedPets(): Flow<List<OwnedPet>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedPets(it) }
}
override fun updateOwnedEquipment(user: User) {
localRepository.updateOwnedEquipment(user)
}
override suspend fun changeOwnedCount(
type: String,
key: String,
amountToAdd: Int,
) {
localRepository.changeOwnedCount(type, key, currentUserID, amountToAdd)
}
override suspend fun sellItem(
type: String,
key: String,
): User? {
val item =
localRepository.getOwnedItem(currentUserID, type, key, true).firstOrNull()
?: return null
return sellItem(item)
}
override suspend fun sellItem(item: OwnedItem): User? {
val itemData =
localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull()
?: return null
return sellItem(itemData, item)
}
override fun getLatestMysteryItem(): Flow<Equipment> {
return localRepository.getLatestMysteryItem()
}
override fun getItem(
type: String,
key: String,
): Flow<Item> {
return localRepository.getItem(type, key)
}
private suspend fun sellItem(
item: Item,
ownedItem: OwnedItem,
): User? {
localRepository.executeTransaction {
val liveItem = localRepository.getLiveObject(ownedItem)
liveItem?.numberOwned = (liveItem?.numberOwned ?: 0) - 1
}
val user = apiClient.sellItem(item.type, item.key) ?: return null
return localRepository.soldItem(currentUserID, user)
}
override suspend fun equipGear(
equipment: String,
asCostume: Boolean,
): Items? {
return equip(if (asCostume) "costume" else "equipped", equipment)
}
override suspend fun equip(
type: String,
key: String,
): Items? {
val liveUser = localRepository.getLiveUser(currentUserID)
if (liveUser != null) {
localRepository.modify(liveUser) { user ->
if (type == "mount") {
user.items?.currentMount = key
} else if (type == "pet") {
user.items?.currentPet = key
}
val outfit =
if (type == "costume") {
user.items?.gear?.costume
} else {
user.items?.gear?.equipped
}
when (key.split("_").firstOrNull()) {
"weapon" -> outfit?.weapon = key
"armor" -> outfit?.armor = key
"shield" -> outfit?.shield = key
"eyewear" -> outfit?.eyeWear = key
"head" -> outfit?.head = key
"back" -> outfit?.back = key
"headAccessory" -> outfit?.headAccessory = key
"body" -> outfit?.body = key
}
}
}
val items = apiClient.equipItem(type, key) ?: return null
if (liveUser == null) return null
localRepository.modify(liveUser) { liveUser ->
val newEquipped = items.gear?.equipped
val oldEquipped = liveUser.items?.gear?.equipped
val newCostume = items.gear?.costume
val oldCostume = liveUser.items?.gear?.costume
newEquipped?.let { equipped -> oldEquipped?.updateWith(equipped) }
newCostume?.let { costume -> oldCostume?.updateWith(costume) }
liveUser.items?.currentMount = items.currentMount
liveUser.items?.currentPet = items.currentPet
liveUser.balance = liveUser.balance
}
return items
}
override suspend fun feedPet(
pet: Pet,
food: Food,
): FeedResponse? {
val feedResponse = apiClient.feedPet(pet.key, food.key) ?: return null
localRepository.feedPet(food.key, pet.key, feedResponse.value ?: 0, currentUserID)
return feedResponse
}
override suspend fun hatchPet(
egg: Egg,
hatchingPotion: HatchingPotion,
successFunction: () -> Unit,
): Items? {
if (appConfigManager.enableLocalChanges()) {
localRepository.hatchPet(egg.key, hatchingPotion.key, currentUserID)
successFunction()
}
val items = apiClient.hatchPet(egg.key, hatchingPotion.key) ?: return null
localRepository.save(items, currentUserID)
if (!appConfigManager.enableLocalChanges()) {
successFunction()
}
return items
}
override suspend fun inviteToQuest(quest: QuestContent): Quest? {
val newQuest = apiClient.inviteToQuest("party", quest.key)
localRepository.changeOwnedCount("quests", quest.key, currentUserID, -1)
return newQuest
}
override suspend fun buyItem(
user: User?,
id: String,
value: Double,
purchaseQuantity: Int,
): BuyResponse? {
val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null
val foundUser = user ?: localRepository.getLiveUser(currentUserID) ?: return buyResponse
val copiedUser = localRepository.getUnmanagedCopy(foundUser)
if (buyResponse.items != null) {
copiedUser.items = buyResponse.items
}
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
import android.content.Context
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.SetupCustomizationRepository
import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User
import javax.inject.Inject
@Suppress("StringLiteralDuplication")
class SetupCustomizationRepositoryImpl @Inject
constructor(private val context: Context) : SetupCustomizationRepository {
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))
private val glasses: List<SetupCustomization>
get() = listOf(SetupCustomization.createGlasses("", R.drawable.creator_blank_face), SetupCustomization.createGlasses("eyewear_special_blackTopFrame", R.drawable.creator_eyewear_special_blacktopframe), SetupCustomization.createGlasses("eyewear_special_blueTopFrame", R.drawable.creator_eyewear_special_bluetopframe), SetupCustomization.createGlasses("eyewear_special_greenTopFrame", R.drawable.creator_eyewear_special_greentopframe), SetupCustomization.createGlasses("eyewear_special_pinkTopFrame", R.drawable.creator_eyewear_special_pinktopframe), SetupCustomization.createGlasses("eyewear_special_redTopFrame", R.drawable.creator_eyewear_special_redtopframe), SetupCustomization.createGlasses("eyewear_special_yellowTopFrame", R.drawable.creator_eyewear_special_yellowtopframe), SetupCustomization.createGlasses("eyewear_special_whiteTopFrame", R.drawable.creator_eyewear_special_whitetopframe))
private val flowers: List<SetupCustomization>
get() = listOf(SetupCustomization.createFlower("0", R.drawable.creator_blank_face), SetupCustomization.createFlower("1", R.drawable.creator_hair_flower_1), SetupCustomization.createFlower("2", R.drawable.creator_hair_flower_2), SetupCustomization.createFlower("3", R.drawable.creator_hair_flower_3), SetupCustomization.createFlower("4", R.drawable.creator_hair_flower_4), SetupCustomization.createFlower("5", R.drawable.creator_hair_flower_5), SetupCustomization.createFlower("6", R.drawable.creator_hair_flower_6))
private val hairColors: List<SetupCustomization>
get() = listOf(SetupCustomization.createHairColor("white", R.color.hair_white), SetupCustomization.createHairColor("brown", R.color.hair_brown), SetupCustomization.createHairColor("blond", R.color.hair_blond), SetupCustomization.createHairColor("red", R.color.hair_red), SetupCustomization.createHairColor("black", R.color.hair_black))
private val sizes: List<SetupCustomization>
get() = listOf(SetupCustomization.createSize("slim", R.drawable.creator_slim_shirt_black, context.getString(R.string.avatar_size_slim)), SetupCustomization.createSize("broad", R.drawable.creator_broad_shirt_black, context.getString(R.string.avatar_size_broad)))
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
}
}
}
package com.habitrpg.android.habitica.data.implementation
import android.content.Context
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.SetupCustomizationRepository
import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User
import javax.inject.Inject
@Suppress("StringLiteralDuplication")
class SetupCustomizationRepositoryImpl
@Inject
constructor(private val context: Context) : SetupCustomizationRepository {
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),
)
private val glasses: List<SetupCustomization>
get() =
listOf(
SetupCustomization.createGlasses("", R.drawable.creator_blank_face),
SetupCustomization.createGlasses(
"eyewear_special_blackTopFrame",
R.drawable.creator_eyewear_special_blacktopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_blueTopFrame",
R.drawable.creator_eyewear_special_bluetopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_greenTopFrame",
R.drawable.creator_eyewear_special_greentopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_pinkTopFrame",
R.drawable.creator_eyewear_special_pinktopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_redTopFrame",
R.drawable.creator_eyewear_special_redtopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_yellowTopFrame",
R.drawable.creator_eyewear_special_yellowtopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_whiteTopFrame",
R.drawable.creator_eyewear_special_whitetopframe,
),
)
private val flowers: List<SetupCustomization>
get() =
listOf(
SetupCustomization.createFlower("0", R.drawable.creator_blank_face),
SetupCustomization.createFlower("1", R.drawable.creator_hair_flower_1),
SetupCustomization.createFlower("2", R.drawable.creator_hair_flower_2),
SetupCustomization.createFlower("3", R.drawable.creator_hair_flower_3),
SetupCustomization.createFlower("4", R.drawable.creator_hair_flower_4),
SetupCustomization.createFlower("5", R.drawable.creator_hair_flower_5),
SetupCustomization.createFlower("6", R.drawable.creator_hair_flower_6),
)
private val hairColors: List<SetupCustomization>
get() =
listOf(
SetupCustomization.createHairColor("white", R.color.hair_white),
SetupCustomization.createHairColor("brown", R.color.hair_brown),
SetupCustomization.createHairColor("blond", R.color.hair_blond),
SetupCustomization.createHairColor("red", R.color.hair_red),
SetupCustomization.createHairColor("black", R.color.hair_black),
)
private val sizes: List<SetupCustomization>
get() =
listOf(
SetupCustomization.createSize(
"slim",
R.drawable.creator_slim_shirt_black,
context.getString(R.string.avatar_size_slim),
),
SetupCustomization.createSize(
"broad",
R.drawable.creator_broad_shirt_black,
context.getString(R.string.avatar_size_broad),
),
)
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)
package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.local.SocialLocalRepository
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.FindUsernameResult
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import java.util.UUID
class SocialRepositoryImpl(
localRepository: SocialLocalRepository,
apiClient: ApiClient,
authenticationHandler: AuthenticationHandler
) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, authenticationHandler), SocialRepository {
override suspend fun transferGroupOwnership(groupID: String, userID: String): Group? {
val group = localRepository.getGroup(groupID).first()?.let { localRepository.getUnmanagedCopy(it) }
group?.leaderID = userID
return group?.let { apiClient.updateGroup(groupID, it) }
}
override suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>? {
apiClient.removeMemberFromGroup(groupID, userID)
return retrievePartyMembers(groupID, true)
}
override suspend fun blockMember(userID: String): List<String>? {
return apiClient.blockMember(userID)
}
override fun getMember(userID: String?): Flow<Member?> {
return localRepository.getMember(userID)
}
override suspend fun updateMember(
memberID: String,
data: Map<String, Map<String, Boolean>>
): Member? {
return apiClient.updateMember(memberID, data)
}
override suspend fun retrievePartySeekingUsers(page: Int): List<Member>? {
return apiClient.retrievePartySeekingUsers(page)
}
override fun getGroupMembership(id: String) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getGroupMembership(it, id) }
override fun getGroupMemberships(): Flow<List<GroupMembership>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getGroupMemberships(it) }
}
override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? {
val messages = apiClient.listGroupChat(groupId)
messages?.forEach { it.groupId = groupId }
return messages
}
override fun getGroupChat(groupId: String): Flow<List<ChatMessage>> {
return localRepository.getGroupChat(groupId)
}
override suspend fun markMessagesSeen(seenGroupId: String) {
apiClient.seenMessages(seenGroupId)
}
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 -> {
val data = mutableMapOf<String, String>()
data["comment"] = additionalInfo
if (groupID?.isNotBlank() != true) {
apiClient.flagInboxMessage(chatMessageID, data)
} else {
apiClient.flagMessage(groupID, chatMessageID, data)
}
}
}
}
override suspend fun reportMember(memberID: String, data: Map<String, String>): Void? {
return apiClient.reportMember(memberID, data)
}
override suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage? {
if (chatMessage.id.isBlank()) {
return null
}
val message = apiClient.likeMessage(chatMessage.groupId ?: "", chatMessage.id)
message?.groupId = chatMessage.groupId
message?.let { localRepository.save(it) }
return message
}
override suspend fun deleteMessage(chatMessage: ChatMessage): Void? {
if (chatMessage.isInboxMessage) {
apiClient.deleteInboxMessage(chatMessage.id)
} else {
apiClient.deleteMessage(chatMessage.groupId ?: "", chatMessage.id)
}
localRepository.deleteMessage(chatMessage.id)
return null
}
override suspend fun postGroupChat(groupId: String, messageObject: HashMap<String, String>): PostChatMessageResult? {
val result = apiClient.postGroupChat(groupId, messageObject)
result?.message?.groupId = groupId
return result
}
override suspend fun postGroupChat(groupId: String, message: String): PostChatMessageResult? {
val messageObject = HashMap<String, String>()
messageObject["message"] = message
return postGroupChat(groupId, messageObject)
}
override suspend fun retrieveGroup(id: String): Group? {
val group = apiClient.getGroup(id)
group?.let { localRepository.saveGroup(it) }
retrieveGroupChat(id)
return group
}
override fun getGroup(id: String?): Flow<Group?> {
if (id?.isNotBlank() != true) {
return emptyFlow()
}
return localRepository.getGroup(id)
}
override suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group? {
if (id?.isNotBlank() != true) {
return null
}
apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges")
localRepository.updateMembership(currentUserID, id, false)
return localRepository.getGroup(id).firstOrNull()
}
override suspend fun joinGroup(id: String?): Group? {
if (id?.isNotBlank() != true) {
return null
}
val group = apiClient.joinGroup(id)
group?.let {
localRepository.updateMembership(currentUserID, id, true)
localRepository.save(group)
}
return group
}
override suspend fun createGroup(
name: String?,
description: String?,
leader: String?,
type: String?,
privacy: String?,
leaderCreateChallenge: Boolean?
): Group? {
val group = Group()
group.name = name
group.description = description
group.type = type
group.leaderID = leader
group.privacy = privacy
val savedGroup = apiClient.createGroup(group)
savedGroup?.let { localRepository.save(it) }
return savedGroup
}
override suspend fun updateGroup(
group: Group?,
name: String?,
description: String?,
leader: String?,
leaderCreateChallenge: Boolean?
): Group? {
if (group == null) {
return null
}
val copiedGroup = localRepository.getUnmanagedCopy(group)
copiedGroup.name = name
copiedGroup.description = description
copiedGroup.leaderID = leader
copiedGroup.leaderOnlyChallenges = leaderCreateChallenge ?: false
localRepository.save(copiedGroup)
return apiClient.updateGroup(copiedGroup.id, copiedGroup)
}
override fun getInboxConversations() = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxConversation(it) }
override fun getInboxMessages(replyToUserID: String?) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxMessages(it, replyToUserID) }
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? {
val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null
messages.forEach {
it.isInboxMessage = true
}
localRepository.saveInboxMessages(currentUserID, uuid, messages, page)
return messages
}
override suspend fun retrieveInboxConversations(): List<InboxConversation>? {
val conversations = apiClient.retrieveInboxConversations() ?: return null
localRepository.saveInboxConversations(currentUserID, conversations)
return conversations
}
override suspend fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): List<ChatMessage>? {
apiClient.postPrivateMessage(messageObject)
return retrieveInboxMessages(recipientId, 0)
}
override suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>? {
val messageObject = HashMap<String, String>()
messageObject["message"] = message
messageObject["toUserId"] = recipientId
return postPrivateMessage(recipientId, messageObject)
}
override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id)
override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
override suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>? {
val members = apiClient.getGroupMembers(id, includeAllPublicFields)
members?.let { localRepository.savePartyMembers(id, it) }
return members
}
override suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>) = apiClient.inviteToGroup(id, inviteData)
override suspend fun retrieveMember(userId: String?, fromHall: Boolean): Member? {
return if (userId == null) {
null
} else {
if (fromHall) {
apiClient.getHallMember(userId)
} else {
try {
val uuid = UUID.fromString(userId).toString()
apiClient.getMember(uuid)
} catch (_: IllegalArgumentException) {
apiClient.getMemberWithUsername(userId)
}
}
}
}
override suspend fun retrievegroupInvites(id: String, includeAllPublicFields: Boolean) = apiClient.getGroupInvites(id, includeAllPublicFields)
override suspend fun findUsernames(username: String, context: String?, id: String?): List<FindUsernameResult>? {
return apiClient.findUsernames(username, context, id)
}
override suspend fun markPrivateMessagesRead(user: User?) {
if (user?.isManaged == true) {
localRepository.modify(user) {
it.inbox?.hasUserSeenInbox = true
}
}
return apiClient.markPrivateMessagesRead()
}
override fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>) {
if (user?.isManaged == true) {
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)
}
}
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.local.SocialLocalRepository
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.FindUsernameResult
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import java.util.UUID
class SocialRepositoryImpl(
localRepository: SocialLocalRepository,
apiClient: ApiClient,
authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, authenticationHandler),
SocialRepository {
override suspend fun transferGroupOwnership(
groupID: String,
userID: String,
): Group? {
val group =
localRepository.getGroup(groupID).first()?.let { localRepository.getUnmanagedCopy(it) }
group?.leaderID = userID
return group?.let { apiClient.updateGroup(groupID, it) }
}
override suspend fun removeMemberFromGroup(
groupID: String,
userID: String,
): List<Member>? {
apiClient.removeMemberFromGroup(groupID, userID)
return retrievePartyMembers(groupID, true)
}
override suspend fun blockMember(userID: String): List<String>? {
return apiClient.blockMember(userID)
}
override fun getMember(userID: String?): Flow<Member?> {
return localRepository.getMember(userID)
}
override suspend fun updateMember(
memberID: String,
data: Map<String, Map<String, Boolean>>,
): Member? {
return apiClient.updateMember(memberID, data)
}
override suspend fun retrievePartySeekingUsers(page: Int): List<Member>? {
return apiClient.retrievePartySeekingUsers(page)
}
override fun getGroupMembership(id: String) =
authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getGroupMembership(
it,
id,
)
}
override fun getGroupMemberships(): Flow<List<GroupMembership>> {
return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getGroupMemberships(
it,
)
}
}
override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? {
val messages = apiClient.listGroupChat(groupId)
messages?.forEach { it.groupId = groupId }
return messages
}
override fun getGroupChat(groupId: String): Flow<List<ChatMessage>> {
return localRepository.getGroupChat(groupId)
}
override suspend fun markMessagesSeen(seenGroupId: String) {
apiClient.seenMessages(seenGroupId)
}
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 -> {
val data = mutableMapOf<String, String>()
data["comment"] = additionalInfo
if (groupID?.isNotBlank() != true) {
apiClient.flagInboxMessage(chatMessageID, data)
} else {
apiClient.flagMessage(groupID, chatMessageID, data)
}
}
}
}
override suspend fun reportMember(
memberID: String,
data: Map<String, String>,
): Void? {
return apiClient.reportMember(memberID, data)
}
override suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage? {
if (chatMessage.id.isBlank()) {
return null
}
val message = apiClient.likeMessage(chatMessage.groupId ?: "", chatMessage.id)
message?.groupId = chatMessage.groupId
message?.let { localRepository.save(it) }
return message
}
override suspend fun deleteMessage(chatMessage: ChatMessage): Void? {
if (chatMessage.isInboxMessage) {
apiClient.deleteInboxMessage(chatMessage.id)
} else {
apiClient.deleteMessage(chatMessage.groupId ?: "", chatMessage.id)
}
localRepository.deleteMessage(chatMessage.id)
return null
}
override suspend fun postGroupChat(
groupId: String,
messageObject: HashMap<String, String>,
): PostChatMessageResult? {
val result = apiClient.postGroupChat(groupId, messageObject)
result?.message?.groupId = groupId
return result
}
override suspend fun postGroupChat(
groupId: String,
message: String,
): PostChatMessageResult? {
val messageObject = HashMap<String, String>()
messageObject["message"] = message
return postGroupChat(groupId, messageObject)
}
override suspend fun retrieveGroup(id: String): Group? {
val group = apiClient.getGroup(id)
group?.let { localRepository.saveGroup(it) }
retrieveGroupChat(id)
return group
}
override fun getGroup(id: String?): Flow<Group?> {
if (id?.isNotBlank() != true) {
return emptyFlow()
}
return localRepository.getGroup(id)
}
override suspend fun leaveGroup(
id: String?,
keepChallenges: Boolean,
): Group? {
if (id?.isNotBlank() != true) {
return null
}
apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges")
localRepository.updateMembership(currentUserID, id, false)
return localRepository.getGroup(id).firstOrNull()
}
override suspend fun joinGroup(id: String?): Group? {
if (id?.isNotBlank() != true) {
return null
}
val group = apiClient.joinGroup(id)
group?.let {
localRepository.updateMembership(currentUserID, id, true)
localRepository.save(group)
}
return group
}
override suspend fun createGroup(
name: String?,
description: String?,
leader: String?,
type: String?,
privacy: String?,
leaderCreateChallenge: Boolean?,
): Group? {
val group = Group()
group.name = name
group.description = description
group.type = type
group.leaderID = leader
group.privacy = privacy
val savedGroup = apiClient.createGroup(group)
savedGroup?.let { localRepository.save(it) }
return savedGroup
}
override suspend fun updateGroup(
group: Group?,
name: String?,
description: String?,
leader: String?,
leaderCreateChallenge: Boolean?,
): Group? {
if (group == null) {
return null
}
val copiedGroup = localRepository.getUnmanagedCopy(group)
copiedGroup.name = name
copiedGroup.description = description
copiedGroup.leaderID = leader
copiedGroup.leaderOnlyChallenges = leaderCreateChallenge ?: false
localRepository.save(copiedGroup)
return apiClient.updateGroup(copiedGroup.id, copiedGroup)
}
override fun getInboxConversations() =
authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxConversation(it) }
override fun getInboxMessages(replyToUserID: String?) =
authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getInboxMessages(
it,
replyToUserID,
)
}
override suspend fun retrieveInboxMessages(
uuid: String,
page: Int,
): List<ChatMessage>? {
val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null
messages.forEach {
it.isInboxMessage = true
}
localRepository.saveInboxMessages(currentUserID, uuid, messages, page)
return messages
}
override suspend fun retrieveInboxConversations(): List<InboxConversation>? {
val conversations = apiClient.retrieveInboxConversations() ?: return null
localRepository.saveInboxConversations(currentUserID, conversations)
return conversations
}
override suspend fun postPrivateMessage(
recipientId: String,
messageObject: HashMap<String, String>,
): List<ChatMessage>? {
apiClient.postPrivateMessage(messageObject)
return retrieveInboxMessages(recipientId, 0)
}
override suspend fun postPrivateMessage(
recipientId: String,
message: String,
): List<ChatMessage>? {
val messageObject = HashMap<String, String>()
messageObject["message"] = message
messageObject["toUserId"] = recipientId
return postPrivateMessage(recipientId, messageObject)
}
override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id)
override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
override suspend fun retrievePartyMembers(
id: String,
includeAllPublicFields: Boolean,
): List<Member>? {
val members = apiClient.getGroupMembers(id, includeAllPublicFields)
members?.let { localRepository.savePartyMembers(id, it) }
return members
}
override suspend fun inviteToGroup(
id: String,
inviteData: Map<String, Any>,
) =
apiClient.inviteToGroup(id, inviteData)
override suspend fun retrieveMember(
userId: String?,
fromHall: Boolean,
): Member? {
return if (userId == null) {
null
} else {
if (fromHall) {
apiClient.getHallMember(userId)
} else {
try {
val uuid = UUID.fromString(userId).toString()
apiClient.getMember(uuid)
} catch (_: IllegalArgumentException) {
apiClient.getMemberWithUsername(userId)
}
}
}
}
override suspend fun retrievegroupInvites(
id: String,
includeAllPublicFields: Boolean,
) =
apiClient.getGroupInvites(id, includeAllPublicFields)
override suspend fun findUsernames(
username: String,
context: String?,
id: String?,
): List<FindUsernameResult>? {
return apiClient.findUsernames(username, context, id)
}
override suspend fun markPrivateMessagesRead(user: User?) {
if (user?.isManaged == true) {
localRepository.modify(user) {
it.inbox?.hasUserSeenInbox = true
}
}
return apiClient.markPrivateMessagesRead()
}
override fun markSomePrivateMessagesAsRead(
user: User?,
messages: List<ChatMessage>,
) {
if (user?.isManaged == true) {
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)
package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TagRepository
import com.habitrpg.android.habitica.data.local.TagLocalRepository
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
class TagRepositoryImpl(
localRepository: TagLocalRepository,
apiClient: ApiClient,
authenticationHandler: AuthenticationHandler
) : BaseRepositoryImpl<TagLocalRepository>(localRepository, apiClient, authenticationHandler),
TagRepository {
override fun getTags() = authenticationHandler.userIDFlow.flatMapLatest { getTags(it) }
override fun getTags(userId: String): Flow<List<Tag>> {
return localRepository.getTags(userId)
}
override suspend fun createTag(tag: Tag): Tag? {
val savedTag = apiClient.createTag(tag) ?: return null
savedTag.userId = currentUserID
localRepository.save(savedTag)
return savedTag
}
override suspend fun updateTag(tag: Tag): Tag? {
val savedTag = apiClient.updateTag(tag.id, tag) ?: return null
savedTag.userId = currentUserID
localRepository.save(savedTag)
return savedTag
}
override suspend fun deleteTag(id: String): Void? {
apiClient.deleteTag(id)
localRepository.deleteTag(id)
return null
}
override suspend fun createTags(tags: Collection<Tag>): List<Tag> {
return tags.mapNotNull {
createTag(it)
}
}
override suspend fun updateTags(tags: Collection<Tag>): List<Tag> {
return tags.mapNotNull {
updateTag(it)
}
}
override suspend fun deleteTags(tagIds: Collection<String>): List<Void> {
return tagIds.mapNotNull {
deleteTag(it)
}
}
}
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TagRepository
import com.habitrpg.android.habitica.data.local.TagLocalRepository
import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
class TagRepositoryImpl(
localRepository: TagLocalRepository,
apiClient: ApiClient,
authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<TagLocalRepository>(localRepository, apiClient, authenticationHandler),
TagRepository {
override fun getTags() = authenticationHandler.userIDFlow.flatMapLatest { getTags(it) }
override fun getTags(userId: String): Flow<List<Tag>> {
return localRepository.getTags(userId)
}
override suspend fun createTag(tag: Tag): Tag? {
val savedTag = apiClient.createTag(tag) ?: return null
savedTag.userId = currentUserID
localRepository.save(savedTag)
return savedTag
}
override suspend fun updateTag(tag: Tag): Tag? {
val savedTag = apiClient.updateTag(tag.id, tag) ?: return null
savedTag.userId = currentUserID
localRepository.save(savedTag)
return savedTag
}
override suspend fun deleteTag(id: String): Void? {
apiClient.deleteTag(id)
localRepository.deleteTag(id)
return null
}
override suspend fun createTags(tags: Collection<Tag>): List<Tag> {
return tags.mapNotNull {
createTag(it)
}
}
override suspend fun updateTags(tags: Collection<Tag>): List<Tag> {
return tags.mapNotNull {
updateTag(it)
}
}
override suspend fun deleteTags(tagIds: Collection<String>): List<Void> {
return tagIds.mapNotNull {
deleteTag(it)
}
}
}

View file

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

View file

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

View file

@ -6,22 +6,36 @@ import com.habitrpg.android.habitica.models.tasks.Task
import kotlinx.coroutines.flow.Flow
interface ChallengeLocalRepository : BaseLocalRepository {
val challenges: Flow<List<Challenge>>
fun getChallenge(id: String): Flow<Challenge>
fun getTasks(challengeID: String): Flow<List<Task>>
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(
challenges: List<Challenge>,
clearChallenges: 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 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 {
fun saveContent(contentResult: ContentResult)
fun saveWorldState(worldState: WorldState)
fun getWorldState(): Flow<WorldState>
}

View file

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

View file

@ -1,70 +1,142 @@
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
import kotlinx.coroutines.flow.Flow
interface InventoryLocalRepository : ContentLocalRepository {
fun getArmoireRemainingCount(): Flow<Int>
fun getOwnedEquipment(): Flow<List<Equipment>>
fun getMounts(): Flow<List<Mount>>
fun getOwnedMounts(userID: String): Flow<List<OwnedMount>>
fun getPets(): Flow<List<Pet>>
fun getOwnedPets(userID: String): Flow<List<OwnedPet>>
fun getInAppRewards(): Flow<List<ShopItem>>
fun getInAppReward(key: String): Flow<ShopItem>
fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getOwnedItems(itemType: String, userID: String, includeZero: Boolean): Flow<List<OwnedItem>>
fun getOwnedItems(userID: String, includeZero: Boolean): Flow<Map<String, OwnedItem>>
fun getEquipmentType(type: String, set: String): Flow<List<Equipment>>
fun getEquipment(key: String): Flow<Equipment>
fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>>
fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>>
fun updateOwnedEquipment(user: User)
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>>
}
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
import kotlinx.coroutines.flow.Flow
interface InventoryLocalRepository : ContentLocalRepository {
fun getArmoireRemainingCount(): Flow<Int>
fun getOwnedEquipment(): Flow<List<Equipment>>
fun getMounts(): Flow<List<Mount>>
fun getOwnedMounts(userID: String): Flow<List<OwnedMount>>
fun getPets(): Flow<List<Pet>>
fun getOwnedPets(userID: String): Flow<List<OwnedPet>>
fun getInAppRewards(): Flow<List<ShopItem>>
fun getInAppReward(key: String): Flow<ShopItem>
fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getOwnedItems(
itemType: String,
userID: String,
includeZero: Boolean,
): Flow<List<OwnedItem>>
fun getOwnedItems(
userID: String,
includeZero: Boolean,
): Flow<Map<String, OwnedItem>>
fun getEquipmentType(
type: String,
set: String,
): Flow<List<Equipment>>
fun getEquipment(key: String): Flow<Equipment>
fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>>
fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>>
fun updateOwnedEquipment(user: User)
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
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface SocialLocalRepository : BaseLocalRepository {
fun getUserGroups(userID: String, type: String?): Flow<List<Group>>
fun getGroup(id: String): Flow<Group?>
fun saveGroup(group: Group)
fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
fun deleteMessage(id: String)
fun getPartyMembers(partyId: String): Flow<List<Member>>
fun getGroupMembers(groupID: String): Flow<List<Member>>
fun updateRSVPNeeded(user: User?, newValue: Boolean)
fun likeMessage(chatMessage: ChatMessage, userId: String, liked: Boolean)
fun savePartyMembers(groupId: String?, members: List<Member>)
fun removeQuest(partyId: String)
fun setQuestActivity(party: Group?, active: Boolean)
fun saveChatMessages(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?>
}
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface SocialLocalRepository : BaseLocalRepository {
fun getUserGroups(
userID: String,
type: String?,
): Flow<List<Group>>
fun getGroup(id: String): Flow<Group?>
fun saveGroup(group: Group)
fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
fun deleteMessage(id: String)
fun getPartyMembers(partyId: String): Flow<List<Member>>
fun getGroupMembers(groupID: String): Flow<List<Member>>
fun updateRSVPNeeded(
user: User?,
newValue: Boolean,
)
fun likeMessage(
chatMessage: ChatMessage,
userId: String,
liked: Boolean,
)
fun savePartyMembers(
groupId: String?,
members: List<Member>,
)
fun removeQuest(partyId: String)
fun setQuestActivity(
party: Group?,
active: Boolean,
)
fun saveChatMessages(
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
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.flow.Flow
interface TaskLocalRepository : BaseLocalRepository {
fun getTasks(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flow<List<Task>>
fun getTasks(userId: String): Flow<List<Task>>
fun saveTasks(ownerID: String, tasksOrder: TasksOrder, tasks: TaskList)
fun deleteTask(taskID: String)
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task>
fun markTaskCompleted(taskId: String, isCompleted: Boolean)
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>>
}
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.flow.Flow
interface TaskLocalRepository : BaseLocalRepository {
fun getTasks(
taskType: TaskType,
userID: String,
includedGroupIDs: Array<String>,
): Flow<List<Task>>
fun getTasks(userId: String): Flow<List<Task>>
fun saveTasks(
ownerID: String,
tasksOrder: TasksOrder,
tasks: TaskList,
)
fun deleteTask(taskID: String)
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task>
fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
)
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
import com.habitrpg.android.habitica.models.TutorialStep
import kotlinx.coroutines.flow.Flow
interface TutorialLocalRepository : BaseLocalRepository {
fun getTutorialStep(key: String): Flow<TutorialStep>
fun getTutorialSteps(keys: List<String>): Flow<List<TutorialStep>>
}
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.TutorialStep
import kotlinx.coroutines.flow.Flow
interface TutorialLocalRepository : BaseLocalRepository {
fun getTutorialStep(key: String): Flow<TutorialStep>
fun getTutorialSteps(keys: List<String>): Flow<List<TutorialStep>>
}

View file

@ -1,32 +1,40 @@
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface UserLocalRepository : BaseLocalRepository {
suspend fun getTutorialSteps(): Flow<RealmResults<TutorialStep>>
fun getUser(userID: String): Flow<User?>
fun saveUser(user: User, overrideExisting: Boolean = true)
fun saveMessages(messages: List<ChatMessage>)
fun getSkills(user: User): Flow<List<Skill>>
fun getSpecialItems(user: User): Flow<List<Skill>>
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?>
}
package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface UserLocalRepository : BaseLocalRepository {
suspend fun getTutorialSteps(): Flow<RealmResults<TutorialStep>>
fun getUser(userID: String): Flow<User?>
fun saveUser(
user: User,
overrideExisting: Boolean = true,
)
fun saveMessages(messages: List<ChatMessage>)
fun getSkills(user: User): Flow<List<Skill>>
fun getSpecialItems(user: User): Flow<List<Skill>>
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
import com.habitrpg.android.habitica.data.local.BaseLocalRepository
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.BaseObject
import com.habitrpg.android.habitica.models.user.User
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmObject
import io.realm.kotlin.deleteFromRealm
import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import java.util.concurrent.atomic.AtomicBoolean
abstract class RealmBaseLocalRepository internal constructor(override var realm: Realm) : BaseLocalRepository {
override val isClosed: Boolean
get() = realm.isClosed
override fun close() {
realm.close()
}
override fun executeTransaction(transaction: (Realm) -> Unit) {
pendingSaves.add(transaction)
if (isSaving.compareAndSet(false, true)) {
process()
}
}
override fun <T : BaseObject> getUnmanagedCopy(managedObject: T): T {
return if (managedObject is RealmObject && managedObject.isManaged && managedObject.isValid) {
realm.copyFromRealm(managedObject)
} else {
managedObject
}
}
override fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T> {
if (isClosed) { return emptyList() }
return realm.copyFromRealm(list)
}
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)
} catch (_: java.lang.IllegalArgumentException) {
}
}
private fun process() {
if (isClosed) { return }
realm.executeTransaction {
while (pendingSaves.isNotEmpty()) {
val pending = pendingSaves.removeFirst()
@Suppress("UNCHECKED_CAST")
if (pending is RealmModel) {
copy(it, pending)
} else if (pending as? List<BaseObject> != null) {
it.insertOrUpdate(pending)
} else if (pending is Function0<*>) {
pending.invoke()
} else if (pending as? Function1<Realm, *> != null) {
pending.invoke(it)
}
}
isSaving.set(false)
}
}
override fun <T : BaseObject> save(obj: T) {
pendingSaves.add(obj)
if (isSaving.compareAndSet(false, true)) {
process()
}
}
override fun <T : BaseObject> save(objects: List<T>) {
pendingSaves.add(objects)
if (isSaving.compareAndSet(false, true)) {
process()
}
}
override fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) {
if (isClosed) { return }
val liveObject = getLiveObject(obj) ?: return
executeTransaction {
transaction(liveObject)
}
}
override fun <T : BaseMainObject> delete(obj: T) {
if (isClosed) { return }
val liveObject = getLiveObject(obj) ?: return
executeTransaction {
liveObject.deleteFromRealm()
}
}
override fun getLiveUser(id: String): User? {
return realm.where(User::class.java).equalTo("id", id).findFirst()
}
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() }
}
}
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.BaseLocalRepository
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.BaseObject
import com.habitrpg.android.habitica.models.user.User
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmObject
import io.realm.kotlin.deleteFromRealm
import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import java.util.concurrent.atomic.AtomicBoolean
abstract class RealmBaseLocalRepository internal constructor(override var realm: Realm) :
BaseLocalRepository {
override val isClosed: Boolean
get() = realm.isClosed
override fun close() {
realm.close()
}
override fun executeTransaction(transaction: (Realm) -> Unit) {
pendingSaves.add(transaction)
if (isSaving.compareAndSet(false, true)) {
process()
}
}
override fun <T : BaseObject> getUnmanagedCopy(managedObject: T): T {
return if (managedObject is RealmObject && managedObject.isManaged && managedObject.isValid) {
realm.copyFromRealm(managedObject)
} else {
managedObject
}
}
override fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T> {
if (isClosed) {
return emptyList()
}
return realm.copyFromRealm(list)
}
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)
} catch (_: java.lang.IllegalArgumentException) {
}
}
private fun process() {
if (isClosed) {
return
}
realm.executeTransaction {
while (pendingSaves.isNotEmpty()) {
val pending = pendingSaves.removeFirst()
@Suppress("UNCHECKED_CAST")
if (pending is RealmModel) {
copy(it, pending)
} else if (pending as? List<BaseObject> != null) {
it.insertOrUpdate(pending)
} else if (pending is Function0<*>) {
pending.invoke()
} else if (pending as? Function1<Realm, *> != null) {
pending.invoke(it)
}
}
isSaving.set(false)
}
}
override fun <T : BaseObject> save(obj: T) {
pendingSaves.add(obj)
if (isSaving.compareAndSet(false, true)) {
process()
}
}
override fun <T : BaseObject> save(objects: List<T>) {
pendingSaves.add(objects)
if (isSaving.compareAndSet(false, true)) {
process()
}
}
override fun <T : BaseMainObject> modify(
obj: T,
transaction: (T) -> Unit,
) {
if (isClosed) {
return
}
val liveObject = getLiveObject(obj) ?: return
executeTransaction {
transaction(liveObject)
}
}
override fun <T : BaseMainObject> delete(obj: T) {
if (isClosed) {
return
}
val liveObject = getLiveObject(obj) ?: return
executeTransaction {
liveObject.deleteFromRealm()
}
}
override fun getLiveUser(id: String): User? {
return realm.where(User::class.java).equalTo("id", id).findFirst()
}
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.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)
.equalTo("userID", userID)
.equalTo("challengeID", challengeID)
.findAll()
.toFlow()
.filter { it.isLoaded }
.map { it.count() > 0 }
override fun getChallengeMembership(
userId: String,
id: String,
) =
realm.where(ChallengeMembership::class.java)
.equalTo("userID", userId)
.equalTo("challengeID", id)
.findAll()
.toFlow()
.filter { it.isLoaded }
.map { it.first() }
.filterNotNull()
override fun getChallengeMembership(userId: String, id: String) = realm.where(ChallengeMembership::class.java)
.equalTo("userID", userId)
.equalTo("challengeID", id)
.findAll()
.toFlow()
.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 getChallengeMemberships(userId: String) =
realm.where(ChallengeMembership::class.java)
.equalTo("userID", userId)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getChallenge(id: String): Flow<Challenge> {
return realm.where(Challenge::class.java)
@ -59,12 +69,13 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
}
override val challenges: Flow<List<Challenge>>
get() = realm.where(Challenge::class.java)
.isNotNull("name")
.sort("official", Sort.DESCENDING, "createdAt", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
get() =
realm.where(Challenge::class.java)
.isNotNull("name")
.sort("official", Sort.DESCENDING, "createdAt", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
@OptIn(ExperimentalCoroutinesApi::class)
override fun getUserChallenges(userId: String): Flow<List<Challenge>> {
@ -74,9 +85,10 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
.toFlow()
.filter { it.isLoaded }
.flatMapLatest { it ->
val ids = it.map {
return@map it.challengeID
}.toTypedArray()
val ids =
it.map {
return@map it.challengeID
}.toTypedArray()
realm.where(Challenge::class.java)
.isNotNull("name")
.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
executeTransaction {
if (isParticipating) {
user.challenges?.add(ChallengeMembership(userID, challengeID))
} 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)
}
}
@ -107,7 +125,7 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
challenges: List<Challenge>,
clearChallenges: Boolean,
memberOnly: Boolean,
userID: String
userID: String,
) {
if (clearChallenges || memberOnly) {
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.map
open class RealmContentLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), ContentLocalRepository {
open class RealmContentLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
ContentLocalRepository {
override fun saveContent(contentResult: ContentResult) {
executeTransaction { realm1 ->
contentResult.potion?.let { realm1.insertOrUpdate(it) }

View file

@ -8,26 +8,33 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import java.util.Date
class RealmCustomizationLocalRepository(realm: Realm) : RealmContentLocalRepository(realm), CustomizationLocalRepository {
override fun getCustomizations(type: String, category: String?, onlyAvailable: Boolean): Flow<List<Customization>> {
var query = realm.where(Customization::class.java)
.equalTo("type", type)
.equalTo("category", category)
class RealmCustomizationLocalRepository(realm: Realm) :
RealmContentLocalRepository(realm),
CustomizationLocalRepository {
override fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>> {
var query =
realm.where(Customization::class.java)
.equalTo("type", type)
.equalTo("category", category)
if (onlyAvailable) {
val today = Date()
query = query
.beginGroup()
.beginGroup()
.lessThanOrEqualTo("availableFrom", today)
.greaterThanOrEqualTo("availableUntil", today)
.endGroup()
.or()
.beginGroup()
.isNull("availableFrom")
.isNull("availableUntil")
.endGroup()
.endGroup()
query =
query
.beginGroup()
.beginGroup()
.lessThanOrEqualTo("availableFrom", today)
.greaterThanOrEqualTo("availableUntil", today)
.endGroup()
.or()
.beginGroup()
.isNull("availableFrom")
.isNull("availableUntil")
.endGroup()
.endGroup()
}
return query
.sort("customizationSet")

View file

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

View file

@ -92,7 +92,10 @@ class RealmInventoryLocalRepository(realm: Realm) :
.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)
.equalTo("type", type)
.equalTo("gearSet", set)
@ -104,17 +107,18 @@ class RealmInventoryLocalRepository(realm: Realm) :
override fun getOwnedItems(
itemType: String,
userID: String,
includeZero: Boolean
includeZero: Boolean,
): Flow<List<OwnedItem>> {
return queryUser(userID).map {
val items = when (itemType) {
"eggs" -> it?.items?.eggs
"hatchingPotions" -> it?.items?.hatchingPotions
"food" -> it?.items?.food
"quests" -> it?.items?.quests
"special" -> it?.items?.special
else -> emptyList()
} ?: emptyList()
val items =
when (itemType) {
"eggs" -> it?.items?.eggs
"hatchingPotions" -> it?.items?.hatchingPotions
"food" -> it?.items?.food
"quests" -> it?.items?.quests
"special" -> it?.items?.special
else -> emptyList()
} ?: emptyList()
if (includeZero) {
items
} else {
@ -128,12 +132,18 @@ class RealmInventoryLocalRepository(realm: Realm) :
.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()
.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)
.filterNotNull()
.map {
@ -168,9 +178,14 @@ class RealmInventoryLocalRepository(realm: Realm) :
.filter { it.isLoaded }
}
override fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> {
var query = realm.where(Mount::class.java)
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING)
override fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>> {
var query =
realm.where(Mount::class.java)
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING)
if (type != null) {
query = query.equalTo("animal", type)
}
@ -202,9 +217,14 @@ class RealmInventoryLocalRepository(realm: Realm) :
.filter { it.isLoaded }
}
override fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> {
var query = realm.where(Pet::class.java)
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING)
override fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>> {
var query =
realm.where(Pet::class.java)
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING)
if (type != null) {
query = query.equalTo("animal", type)
}
@ -239,7 +259,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
type: String,
key: String,
userID: String,
amountToAdd: Int
amountToAdd: Int,
) {
val item = getOwnedItem(userID, type, key, true).firstOrNull()
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
amountToAdd?.let { amount ->
executeTransaction { liveItem.numberOwned = liveItem.numberOwned + amount }
@ -258,7 +281,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
userID: String,
type: String,
key: String,
includeZero: Boolean
includeZero: Boolean,
): Flow<OwnedItem> {
return queryUser(userID)
.filterNotNull()
@ -271,7 +294,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
"quests" -> it.items?.quests
else -> emptyList()
} ?: emptyList()
)
)
items = items.filter { it.key == key }
if (includeZero) {
items
@ -283,15 +306,19 @@ class RealmInventoryLocalRepository(realm: Realm) :
.map { it.first() }
}
override fun getItem(type: String, key: String): Flow<Item> {
val itemClass: Class<out RealmObject> = when (type) {
"eggs" -> Egg::class.java
"hatchingPotions" -> HatchingPotion::class.java
"food" -> Food::class.java
"quests" -> QuestContent::class.java
"special" -> SpecialItem::class.java
else -> Egg::class.java
}
override fun getItem(
type: String,
key: String,
): Flow<Item> {
val itemClass: Class<out RealmObject> =
when (type) {
"eggs" -> 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)
.findAll()
.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()
newPet.key = "$eggKey-$potionKey"
newPet.trained = 5
@ -369,7 +400,10 @@ class RealmInventoryLocalRepository(realm: Realm) :
.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
items.setItemTypes()
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 user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: 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 pet = user.items?.pets?.firstOrNull { it.key == petKey } ?: 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 {
val user = realm.where(User::class.java)
.equalTo("id", userID)
.findFirst() ?: return updatedUser
override fun soldItem(
userID: String,
updatedUser: User,
): User {
val user =
realm.where(User::class.java)
.equalTo("id", userID)
.findFirst() ?: return updatedUser
executeTransaction {
val items = updatedUser.items
if (items != null) {
@ -453,7 +500,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
realm.where(Food::class.java)
.lessThan("event.start", Date())
.greaterThan("event.end", Date())
.findAll().toFlow()
.findAll().toFlow(),
) { items, food ->
items.addAll(food)
items
@ -462,7 +509,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
realm.where(HatchingPotion::class.java)
.lessThan("event.start", Date())
.greaterThan("event.end", Date())
.findAll().toFlow()
.findAll().toFlow(),
) { items, food ->
items.addAll(food)
items
@ -471,7 +518,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
realm.where(QuestContent::class.java)
.lessThan("event.start", Date())
.greaterThan("event.end", Date())
.findAll().toFlow()
.findAll().toFlow(),
) { items, food ->
items.addAll(food)
items

View file

@ -18,27 +18,39 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
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)
.equalTo("userID", userId)
.equalTo("groupID", id)
.findAll()
.toFlow()
.filter { it.isLoaded && it.isNotEmpty() }
.map { it.first() }
override fun getGroupMemberships(userId: String): Flow<List<GroupMembership>> =
realm.where(GroupMembership::class.java)
.equalTo("userID", userId)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getGroupMemberships(userId: String): Flow<List<GroupMembership>> = realm.where(GroupMembership::class.java)
.equalTo("userID", userId)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun updateMembership(userId: String, id: String, isMember: Boolean) {
override fun updateMembership(
userId: String,
id: String,
isMember: Boolean,
) {
if (isMember) {
save(GroupMembership(userId, id))
} 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) {
executeTransaction {
membership.deleteFromRealm()
@ -61,19 +73,22 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
userID: String,
recipientID: String,
messages: List<ChatMessage>,
page: Int
page: Int,
) {
messages.forEach { it.userID = userID }
for (message in messages) {
val existingMessage = realm.where(ChatMessage::class.java)
.equalTo("id", message.id)
.findAll()
.firstOrNull()
val existingMessage =
realm.where(ChatMessage::class.java)
.equalTo("id", message.id)
.findAll()
.firstOrNull()
message.isSeen = existingMessage != null
}
save(messages)
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>()
for (existingMessage in existingMessages) {
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 }
save(conversations)
val existingConversations = realm.where(InboxConversation::class.java).findAll()
@ -111,10 +129,14 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
.map { member -> member.firstOrNull() }
}
override fun saveGroupMemberships(userID: String?, memberships: List<GroupMembership>) {
override fun saveGroupMemberships(
userID: String?,
memberships: List<GroupMembership>,
) {
save(memberships)
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>()
for (existingMembership in existingMemberships) {
val isStillMember = memberships.any { existingMembership.groupID == it.groupID }
@ -129,24 +151,28 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun getUserGroups(userID: String, type: String?) = realm.where(GroupMembership::class.java)
.equalTo("userID", userID)
.findAll()
.toFlow()
.filter { it.isLoaded }
.flatMapLatest { memberships ->
realm.where(Group::class.java)
.equalTo("type", type ?: "guild")
.`in`(
"id",
memberships.map {
return@map it.groupID
}.toTypedArray()
)
.sort("memberCount", Sort.DESCENDING)
.findAll()
.toFlow()
}
override fun getUserGroups(
userID: String,
type: String?,
) =
realm.where(GroupMembership::class.java)
.equalTo("userID", userID)
.findAll()
.toFlow()
.filter { it.isLoaded }
.flatMapLatest { memberships ->
realm.where(Group::class.java)
.equalTo("type", type ?: "guild")
.`in`(
"id",
memberships.map {
return@map it.groupID
}.toTypedArray(),
)
.sort("memberCount", Sort.DESCENDING)
.findAll()
.toFlow()
}
override fun getGroup(id: String): Flow<Group?> {
return realm.where(Group::class.java)
@ -171,23 +197,32 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
executeTransaction { chatMessage?.deleteFromRealm() }
}
override fun getPartyMembers(partyId: String) = realm.where(Member::class.java)
.equalTo("party.id", partyId)
.findAll()
.toFlow()
override fun getPartyMembers(partyId: String) =
realm.where(Member::class.java)
.equalTo("party.id", partyId)
.findAll()
.toFlow()
override fun getGroupMembers(groupID: String) = realm.where(GroupMembership::class.java)
.equalTo("groupID", groupID)
.findAll()
.toFlow()
.map { memberships -> memberships.map { it.userID }.toTypedArray() }
.flatMapLatest { realm.where(Member::class.java).`in`("id", it).findAll().toFlow() }
override fun getGroupMembers(groupID: String) =
realm.where(GroupMembership::class.java)
.equalTo("groupID", groupID)
.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 }
}
override fun likeMessage(chatMessage: ChatMessage, userId: String, liked: Boolean) {
override fun likeMessage(
chatMessage: ChatMessage,
userId: String,
liked: Boolean,
) {
val liveMessage = getLiveObject(chatMessage)
if (liveMessage == null) {
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)
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>()
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) {
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()
executeTransaction {
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
val liveParty = getLiveObject(party)
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)
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>()
for (existingMessage in existingMessages) {
val isStillMember = chatMessages.any { existingMessage.id == it.id }
@ -279,19 +329,24 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
return party != null && party.isValid
}
override fun getInboxMessages(userId: String, replyToUserID: String?) = realm.where(ChatMessage::class.java)
.equalTo("isInboxMessage", true)
.equalTo("uuid", replyToUserID)
.equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getInboxMessages(
userId: String,
replyToUserID: String?,
) =
realm.where(ChatMessage::class.java)
.equalTo("isInboxMessage", true)
.equalTo("uuid", replyToUserID)
.equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getInboxConversation(userId: String) = realm.where(InboxConversation::class.java)
.equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getInboxConversation(userId: String) =
realm.where(InboxConversation::class.java)
.equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
}

View file

@ -1,254 +1,294 @@
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.TaskLocalRepository
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.RemindersItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import io.realm.Realm
import io.realm.RealmResults
import io.realm.Sort
import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TaskLocalRepository {
override fun getTasks(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow()
return findTasks(taskType, userID)
.toFlow()
.filter { it.isLoaded }
}
private fun findTasks(
taskType: TaskType,
ownerID: String
): RealmResults<Task> {
return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value)
.equalTo("ownerID", ownerID)
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
.findAll()
}
override fun getTasks(userId: String): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow()
return realm.where(Task::class.java).equalTo("ownerID", userId)
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
}
override fun saveTasks(ownerID: String, tasksOrder: TasksOrder, tasks: TaskList) {
val sortedTasks = mutableListOf<Task>()
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.habits))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.dailys))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.todos))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.rewards))
for (task in tasks.tasks.values) {
task.position = (sortedTasks.lastOrNull { it.type == task.type }?.position ?: -1) + 1
sortedTasks.add(task)
}
removeOldTasks(ownerID, sortedTasks)
val allChecklistItems = ArrayList<ChecklistItem>()
val allReminders = ArrayList<RemindersItem>()
sortedTasks.forEach {
if (it.ownerID.isBlank()) {
it.ownerID = ownerID
}
it.checklist?.let { it1 -> allChecklistItems.addAll(it1) }
it.reminders?.let { it1 -> allReminders.addAll(it1) }
}
removeOldReminders(allReminders)
removeOldChecklists(allChecklistItems)
executeTransaction { realm1 -> realm1.insertOrUpdate(sortedTasks) }
}
override fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>) {
removeCompletedTodos(userId, tasks)
executeTransaction { realm1 -> realm1.insertOrUpdate(tasks) }
}
private fun removeOldChecklists(onlineItems: List<ChecklistItem>) {
val localItems = realm.where(ChecklistItem::class.java).findAll().createSnapshot()
val itemsToDelete = localItems.filterNot { onlineItems.contains(it) }
realm.executeTransaction {
for (item in itemsToDelete) {
item.deleteFromRealm()
}
}
}
private fun removeOldReminders(onlineReminders: List<RemindersItem>) {
val localReminders = realm.where(RemindersItem::class.java).findAll().createSnapshot()
val itemsToDelete = localReminders.filterNot { onlineReminders.contains(it) }
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
for (taskId in taskOrder) {
val task = taskMap[taskId]
if (task != null) {
task.position = position
taskList.add(task)
position++
taskMap.remove(taskId)
}
}
return taskList
}
private fun removeOldTasks(ownerID: String, onlineTaskList: List<Task>) {
if (realm.isClosed) return
val localTasks = realm.where(Task::class.java)
.equalTo("ownerID", ownerID)
.beginGroup()
.beginGroup()
.equalTo("typeValue", TaskType.TODO.value)
.equalTo("completed", false)
.endGroup()
.or()
.notEqualTo("typeValue", TaskType.TODO.value)
.endGroup()
.findAll()
.createSnapshot()
val tasksToDelete = localTasks.filterNot { onlineTaskList.contains(it) }
executeTransaction {
for (localTask in tasksToDelete) {
localTask.deleteFromRealm()
}
}
}
private fun removeCompletedTodos(userID: String, onlineTaskList: MutableCollection<Task>) {
val localTasks = realm.where(Task::class.java)
.equalTo("ownerID", userID)
.equalTo("typeValue", TaskType.TODO.value)
.equalTo("completed", true)
.findAll()
.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()
}
}
}
override fun getTask(taskId: String): Flow<Task> {
if (realm.isClosed) {
return emptyFlow()
}
return realm.where(Task::class.java).equalTo("id", taskId).findAll().toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.filterNotNull()
}
override fun getTaskCopy(taskId: String): Flow<Task> {
return getTask(taskId)
.map { task ->
return@map if (task.isManaged && task.isValid) {
realm.copyFromRealm(task)
} else {
task
}
}
}
override fun markTaskCompleted(taskId: String, isCompleted: Boolean) {
val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst()
executeTransaction { task?.completed = true }
}
override fun swapTaskPosition(firstPosition: Int, secondPosition: Int) {
val firstTask = realm.where(Task::class.java).equalTo("position", firstPosition).findFirst()
val secondTask = 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 getTaskAtPosition(taskType: String, position: Int): Flow<Task> {
return realm.where(Task::class.java).equalTo("typeValue", taskType).equalTo("position", position)
.findAll()
.toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.filterNotNull()
}
override fun updateIsdue(daily: TaskList): TaskList {
val tasks = realm.where(Task::class.java).equalTo("typeValue", TaskType.DAILY.value).findAll()
realm.beginTransaction()
tasks.filter { daily.tasks.containsKey(it.id) }.forEach { it.isDue = daily.tasks[it.id]?.isDue }
realm.commitTransaction()
return daily
}
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 }
}
}
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.TaskLocalRepository
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.RemindersItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import io.realm.Realm
import io.realm.RealmResults
import io.realm.Sort
import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
class RealmTaskLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
TaskLocalRepository {
override fun getTasks(
taskType: TaskType,
userID: String,
includedGroupIDs: Array<String>,
): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow()
return findTasks(taskType, userID)
.toFlow()
.filter { it.isLoaded }
}
private fun findTasks(
taskType: TaskType,
ownerID: String,
): RealmResults<Task> {
return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value)
.equalTo("ownerID", ownerID)
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
.findAll()
}
override fun getTasks(userId: String): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow()
return realm.where(Task::class.java).equalTo("ownerID", userId)
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
}
override fun saveTasks(
ownerID: String,
tasksOrder: TasksOrder,
tasks: TaskList,
) {
val sortedTasks = mutableListOf<Task>()
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.habits))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.dailys))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.todos))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.rewards))
for (task in tasks.tasks.values) {
task.position = (sortedTasks.lastOrNull { it.type == task.type }?.position ?: -1) + 1
sortedTasks.add(task)
}
removeOldTasks(ownerID, sortedTasks)
val allChecklistItems = ArrayList<ChecklistItem>()
val allReminders = ArrayList<RemindersItem>()
sortedTasks.forEach {
if (it.ownerID.isBlank()) {
it.ownerID = ownerID
}
it.checklist?.let { it1 -> allChecklistItems.addAll(it1) }
it.reminders?.let { it1 -> allReminders.addAll(it1) }
}
removeOldReminders(allReminders)
removeOldChecklists(allChecklistItems)
executeTransaction { realm1 -> realm1.insertOrUpdate(sortedTasks) }
}
override fun saveCompletedTodos(
userId: String,
tasks: MutableCollection<Task>,
) {
removeCompletedTodos(userId, tasks)
executeTransaction { realm1 -> realm1.insertOrUpdate(tasks) }
}
private fun removeOldChecklists(onlineItems: List<ChecklistItem>) {
val localItems = realm.where(ChecklistItem::class.java).findAll().createSnapshot()
val itemsToDelete = localItems.filterNot { onlineItems.contains(it) }
realm.executeTransaction {
for (item in itemsToDelete) {
item.deleteFromRealm()
}
}
}
private fun removeOldReminders(onlineReminders: List<RemindersItem>) {
val localReminders = realm.where(RemindersItem::class.java).findAll().createSnapshot()
val itemsToDelete = localReminders.filterNot { onlineReminders.contains(it) }
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
for (taskId in taskOrder) {
val task = taskMap[taskId]
if (task != null) {
task.position = position
taskList.add(task)
position++
taskMap.remove(taskId)
}
}
return taskList
}
private fun removeOldTasks(
ownerID: String,
onlineTaskList: List<Task>,
) {
if (realm.isClosed) return
val localTasks =
realm.where(Task::class.java)
.equalTo("ownerID", ownerID)
.beginGroup()
.beginGroup()
.equalTo("typeValue", TaskType.TODO.value)
.equalTo("completed", false)
.endGroup()
.or()
.notEqualTo("typeValue", TaskType.TODO.value)
.endGroup()
.findAll()
.createSnapshot()
val tasksToDelete = localTasks.filterNot { onlineTaskList.contains(it) }
executeTransaction {
for (localTask in tasksToDelete) {
localTask.deleteFromRealm()
}
}
}
private fun removeCompletedTodos(
userID: String,
onlineTaskList: MutableCollection<Task>,
) {
val localTasks =
realm.where(Task::class.java)
.equalTo("ownerID", userID)
.equalTo("typeValue", TaskType.TODO.value)
.equalTo("completed", true)
.findAll()
.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()
}
}
}
override fun getTask(taskId: String): Flow<Task> {
if (realm.isClosed) {
return emptyFlow()
}
return realm.where(Task::class.java).equalTo("id", taskId).findAll().toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.filterNotNull()
}
override fun getTaskCopy(taskId: String): Flow<Task> {
return getTask(taskId)
.map { task ->
return@map if (task.isManaged && task.isValid) {
realm.copyFromRealm(task)
} else {
task
}
}
}
override fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
) {
val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst()
executeTransaction { task?.completed = true }
}
override fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
) {
val firstTask = realm.where(Task::class.java).equalTo("position", firstPosition).findFirst()
val secondTask =
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 getTaskAtPosition(
taskType: String,
position: Int,
): Flow<Task> {
return realm.where(Task::class.java).equalTo("typeValue", taskType)
.equalTo("position", position)
.findAll()
.toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.filterNotNull()
}
override fun updateIsdue(daily: TaskList): TaskList {
val tasks =
realm.where(Task::class.java).equalTo("typeValue", TaskType.DAILY.value).findAll()
realm.beginTransaction()
tasks.filter { daily.tasks.containsKey(it.id) }
.forEach { it.isDue = daily.tasks[it.id]?.isDue }
realm.commitTransaction()
return daily
}
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.map
class RealmTutorialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TutorialLocalRepository {
class RealmTutorialLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
TutorialLocalRepository {
override fun getTutorialStep(key: String): Flow<TutorialStep> {
if (realm.isClosed) return emptyFlow()
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?.progress?.collect?.isNotEmpty()
?: false -> UserQuestStatus.QUEST_COLLECT
(it.quest?.progress?.hp ?: 0.0) > 0.0 -> UserQuestStatus.QUEST_BOSS
else -> UserQuestStatus.QUEST_UNKNOWN
}
@ -81,11 +82,15 @@ class RealmUserLocalRepository(realm: Realm) :
.map { users -> users.first() }
}
override fun saveUser(user: User, overrideExisting: Boolean) {
override fun saveUser(
user: User,
overrideExisting: Boolean,
) {
if (realm.isClosed) return
val oldUser = realm.where(User::class.java)
.equalTo("id", user.id)
.findFirst()
val oldUser =
realm.where(User::class.java)
.equalTo("id", user.id)
.findFirst()
if (oldUser != null && oldUser.isValid) {
if (user.needsCron && !oldUser.needsCron) {
if (user.lastCron?.before(oldUser.lastCron) == true) {
@ -101,7 +106,10 @@ class RealmUserLocalRepository(realm: Realm) :
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 tagsToDelete = tags.filterNot { onlineTags.contains(it) }
executeTransaction {

View file

@ -5,21 +5,21 @@ import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
fun HabiticaAlertDialog.addOkButton(
isPrimary: Boolean = true,
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null,
) {
this.addButton(R.string.ok, isPrimary, false, true, listener)
}
fun HabiticaAlertDialog.addCloseButton(
isPrimary: Boolean = false,
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null,
) {
this.addButton(R.string.close, isPrimary, false, true, listener)
}
fun HabiticaAlertDialog.addCancelButton(
isPrimary: Boolean = false,
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null,
) {
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)
}
fun getTranslatedAnimalType(c: Context?, type: String?): String? {
fun getTranslatedAnimalType(
c: Context?,
type: String?,
): String? {
if (c == null) {
return type
}

View file

@ -4,5 +4,8 @@ import android.content.Context
import android.content.res.TypedArray
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)

View file

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

View file

@ -3,24 +3,50 @@ package com.habitrpg.android.habitica.extensions
import android.text.Editable
import android.text.TextWatcher
class OnChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) : TextWatcher {
override fun afterTextChanged(s: Editable?) { /* no-on */ }
class OnChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) :
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)
}
}
class BeforeChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) : TextWatcher {
override fun afterTextChanged(s: Editable?) { /* no-on */ }
class BeforeChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) :
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)
}
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 {
@ -28,7 +54,19 @@ class AfterChangeTextWatcher(private var function: (Editable?) -> Unit) : TextWa
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.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) {
statusBarColor = color
@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 {
statusBarColor = context.getThemeColor(R.attr.colorPrimaryDark)
}

View file

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

View file

@ -23,7 +23,8 @@ import kotlin.time.toDuration
enum class AdType {
ARMOIRE,
SPELL,
FAINT;
FAINT,
;
val adUnitID: String
get() {
@ -60,14 +61,14 @@ fun String.md5(): String? {
}
class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boolean) -> Unit) {
//private var rewardedAd: RewardedAd? = null
// private var rewardedAd: RewardedAd? = null
companion object {
private enum class AdStatus {
UNINITIALIZED,
INITIALIZING,
READY,
DISABLED
DISABLED,
}
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 (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
val androidId: String =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
val deviceId: String = androidId.md5()?.uppercase() ?: ""
//val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(deviceId)).build()
//MobileAds.setRequestConfiguration(configuration)
// val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(deviceId)).build()
// MobileAds.setRequestConfiguration(configuration)
}
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) {
AdStatus.READY -> {
onComplete()
}
AdStatus.DISABLED -> {
return
}
AdStatus.UNINITIALIZED -> {
initialize(context) {
onComplete()
}
}
AdStatus.INITIALIZING -> {
return
}
@ -189,24 +199,27 @@ class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boo
AdStatus.READY -> {
showRewardedAd()
}
AdStatus.DISABLED -> {
rewardAction(false)
return
}
AdStatus.UNINITIALIZED -> {
initialize(activity) {
showRewardedAd()
}
}
AdStatus.INITIALIZING -> {
return
}
}
}
//private fun configureReward() {
//rewardedAd?.run { }
//}
// private fun configureReward() {
// rewardedAd?.run { }
// }
private fun showRewardedAd() {
if (nextAdAllowedDate(type)?.after(Date()) == true) {

View file

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

View file

@ -17,8 +17,8 @@ import com.habitrpg.common.habitica.helpers.launchCatching
import kotlinx.coroutines.MainScope
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
init {
@ -122,7 +122,12 @@ class AppConfigManager(contentRepository: ContentRepository?) : com.habitrpg.com
if (worldState?.isValid == true) {
for (event in worldState?.events ?: listOf(worldState?.currentEvent)) {
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) {
promo = thisPromo
}
@ -178,7 +183,8 @@ class AppConfigManager(contentRepository: ContentRepository?) : com.habitrpg.com
}
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 }
}

View file

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

View file

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

View file

@ -15,15 +15,18 @@ import kotlin.coroutines.EmptyCoroutineContext
@Composable
fun <T> rememberFlow(
flow: Flow<T>,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
): 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
fun <T : R, R> Flow<T>.collectAsStateLifecycleAware(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
context: CoroutineContext = EmptyCoroutineContext,
): State<R> {
val lifecycleAwareFlow = rememberFlow(flow = this)
return lifecycleAwareFlow.collectAsState(initial = initial, context = context)

View file

@ -10,21 +10,41 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class NotificationOpenHandler {
companion object {
fun handleOpenedByNotification(identifier: String, intent: Intent) {
fun handleOpenedByNotification(
identifier: String,
intent: Intent,
) {
MainScope().launch(context = Dispatchers.Main) {
when (identifier) {
PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY -> openNoPartyScreen()
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY -> openPartyScreen()
PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY -> openPartyScreen()
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY -> openGuildDetailScreen(intent.getStringExtra("groupID"))
PushNotificationManager.RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY -> openPrivateMessageScreen(intent.getStringExtra("replyToUUID"), intent.getStringExtra("replyToUsername"))
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY ->
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.GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY -> openSubscriptionScreen()
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY -> handleChatMessage(intent.getStringExtra("type"), intent.getStringExtra("groupID"))
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY -> handleChatMessage(intent.getStringExtra("type"), intent.getStringExtra("groupID"))
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY ->
handleChatMessage(
intent.getStringExtra("type"),
intent.getStringExtra("groupID"),
)
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY ->
handleChatMessage(
intent.getStringExtra("type"),
intent.getStringExtra("groupID"),
)
PushNotificationManager.G1G1_PROMO_KEY -> openGiftOneGetOneInfoScreen()
else -> {
intent.getStringExtra("openURL")?.let {
@ -36,12 +56,21 @@ class NotificationOpenHandler {
}
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) {
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 {
MainNavigationController.navigate(R.id.inboxFragment)
}
@ -49,7 +78,10 @@ class NotificationOpenHandler {
private fun openPartyScreen(isChatNotification: Boolean = false) {
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() {
@ -72,7 +104,10 @@ class NotificationOpenHandler {
MainNavigationController.navigate(R.id.prefsActivity)
}
private fun handleChatMessage(type: String?, groupID: String?) {
private fun handleChatMessage(
type: String?,
groupID: String?,
) {
when (type) {
"party" -> openPartyScreen()
"guild" -> openGuildDetailScreen(groupID)

View file

@ -20,20 +20,26 @@ interface NotificationsManager {
var apiClient: WeakReference<ApiClient>?
fun setNotifications(current: List<Notification>)
fun getNotifications(): Flow<List<Notification>>
fun getNotification(id: String): Notification?
fun dismissTaskNotification(context: Context, task: Task)
fun dismissTaskNotification(
context: Context,
task: Task,
)
}
class MainNotificationsManager : NotificationsManager {
private val seenNotifications: MutableMap<String, Boolean>
override var apiClient: WeakReference<ApiClient>? = null
private var lastNotificationHandling: Date? = null
private val notificationsFlow = MutableStateFlow<List<Notification>?>(null)
private val displayedNotificationEvents = Channel<Notification>()
override val displayNotificationEvents: Flow<Notification> = displayedNotificationEvents.receiveAsFlow().filterNotNull()
override val displayNotificationEvents: Flow<Notification> =
displayedNotificationEvents.receiveAsFlow().filterNotNull()
init {
this.seenNotifications = HashMap()
@ -52,7 +58,10 @@ class MainNotificationsManager : NotificationsManager {
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())
}
@ -65,46 +74,47 @@ class MainNotificationsManager : NotificationsManager {
notifications
.filter { !this.seenNotifications.containsKey(it.id) }
.map {
val notificationDisplayed = when (it.type) {
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true
Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> true
Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> true
Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> true
Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> true
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> true
val notificationDisplayed =
when (it.type) {
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true
Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> true
Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> true
Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> true
Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> true
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> true
Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> true
Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> true
Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> true
Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> true
Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> true
Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> true
Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> true
Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> true
Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> true
Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> true
Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> true
Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> true
Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> true
Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> true
Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> true
Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> true
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> true
Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> true
Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> true
Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> true
Notification.Type.ACHIEVEMENT_SEEING_RED.type -> true
Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> true
Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> true
Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> true
Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> true
Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> true
Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> true
Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> true
Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> true
Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> true
Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> true
Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> true
Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> true
Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> true
Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> true
Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> true
Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> true
Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> true
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> true
Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> true
Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> true
Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> true
Notification.Type.ACHIEVEMENT_SEEING_RED.type -> true
Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> true
Notification.Type.ACHIEVEMENT_GENERIC.type -> true
Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> true
Notification.Type.LOGIN_INCENTIVE.type -> true
Notification.Type.NEW_MYSTERY_ITEMS.type -> true
Notification.Type.FIRST_DROP.type -> true
else -> false
}
Notification.Type.ACHIEVEMENT_GENERIC.type -> true
Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> true
Notification.Type.LOGIN_INCENTIVE.type -> true
Notification.Type.NEW_MYSTERY_ITEMS.type -> true
Notification.Type.FIRST_DROP.type -> true
else -> false
}
if (notificationDisplayed) {
readNotification(it)

View file

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

View file

@ -1,30 +1,36 @@
package com.habitrpg.android.habitica.helpers
object PurchaseTypes {
const val JubilantGrphatrice = "com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant"
const val Purchase4Gems = "com.habitrpg.android.habitica.iap.4gems"
const val Purchase21Gems = "com.habitrpg.android.habitica.iap.21gems"
const val Purchase42Gems = "com.habitrpg.android.habitica.iap.42gems"
const val Purchase84Gems = "com.habitrpg.android.habitica.iap.84gems"
val allGemTypes = listOf(Purchase4Gems, Purchase21Gems, Purchase42Gems, Purchase84Gems)
const val Subscription1Month = "com.habitrpg.android.habitica.subscription.1month"
const val Subscription3Month = "com.habitrpg.android.habitica.subscription.3month"
const val Subscription6Month = "com.habitrpg.android.habitica.subscription.6month"
const val Subscription12Month = "com.habitrpg.android.habitica.subscription.12month"
val allSubscriptionTypes = mutableListOf(
Subscription1Month,
Subscription3Month,
Subscription6Month,
Subscription12Month
)
const val Subscription1MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.1month"
const val Subscription3MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.3month"
const val Subscription6MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.6month"
const val Subscription12MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.12month"
var allSubscriptionNoRenewTypes = listOf(
Subscription1MonthNoRenew,
Subscription3MonthNoRenew,
Subscription6MonthNoRenew,
Subscription12MonthNoRenew
)
const val JUBILANT_GRYPHATRICE = "com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant"
const val PURCHASE_4_GEMS = "com.habitrpg.android.habitica.iap.4gems"
const val PURCHASE_21_GEMS = "com.habitrpg.android.habitica.iap.21gems"
const val PURCHASE_42_GEMS = "com.habitrpg.android.habitica.iap.42gems"
const val PURCHASE_84_GEMS = "com.habitrpg.android.habitica.iap.84gems"
val allGemTypes = listOf(PURCHASE_4_GEMS, PURCHASE_21_GEMS, PURCHASE_42_GEMS, PURCHASE_84_GEMS)
const val SUBSCRIPTION_1_MONTH = "com.habitrpg.android.habitica.subscription.1month"
const val SUBSCRIPTION_3_MONTH = "com.habitrpg.android.habitica.subscription.3month"
const val SUBSCRIPTION_6_MONTH = "com.habitrpg.android.habitica.subscription.6month"
const val SUBSCRIPTION_12_MONTH = "com.habitrpg.android.habitica.subscription.12month"
val allSubscriptionTypes =
mutableListOf(
SUBSCRIPTION_1_MONTH,
SUBSCRIPTION_3_MONTH,
SUBSCRIPTION_6_MONTH,
SUBSCRIPTION_12_MONTH,
)
const val SUBSCRIPTION_1_MONTH_NORENEW =
"com.habitrpg.android.habitica.norenew_subscription.1month"
const val SUBSCRIPTION_3_MONTH_NORENEW =
"com.habitrpg.android.habitica.norenew_subscription.3month"
const val SUBSCRIPTION_6_MONTH_NORENEW =
"com.habitrpg.android.habitica.norenew_subscription.6month"
const val SUBSCRIPTION_12_MONTH_NORENEW =
"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
class ReviewManager(context: Context, private val configManager: AppConfigManager) {
private val reviewManager = ReviewManagerFactory.create(context)
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)
}
fun requestReview(activity: AppCompatActivity, currentCheckins: Int) {
fun requestReview(
activity: AppCompatActivity,
currentCheckins: Int,
) {
if (!canRequestReview(currentCheckins)) return
val request = reviewManager.requestReviewFlow()

View file

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

View file

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

View file

@ -6,64 +6,66 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SoundManager @Inject constructor(var soundFileLoader: SoundFileLoader) {
var soundTheme: String = SoundThemeOff
class SoundManager
@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() {
loadedSoundFiles.clear()
if (soundTheme == SoundThemeOff) {
return
}
fun preloadAllFiles() {
loadedSoundFiles.clear()
if (soundTheme == SOUND_THEME_OFF) {
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>()
soundFiles.add(SoundFile(soundTheme, type))
soundFiles.add(SoundFile(soundTheme, SOUND_ACHIEVEMENT_UNLOCKED))
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 {
val newFiles = soundFileLoader.download(soundFiles)
val file = newFiles[0]
loadedSoundFiles[type] = file
file.play()
soundFileLoader.download(soundFiles)
}
}
}
companion object {
const val SoundAchievementUnlocked = "Achievement_Unlocked"
const val SoundChat = "Chat"
const val SoundDaily = "Daily"
const val SoundDeath = "Death"
const val SoundItemDrop = "Item_Drop"
const val SoundLevelUp = "Level_Up"
const val SoundMinusHabit = "Minus_Habit"
const val SoundPlusHabit = "Plus_Habit"
const val SoundReward = "Reward"
const val SoundTodo = "Todo"
const val SoundThemeOff = "off"
fun loadAndPlayAudio(type: String) {
if (soundTheme == SOUND_THEME_OFF) {
return
}
if (loadedSoundFiles.containsKey(type)) {
loadedSoundFiles[type]?.play()
} else {
val soundFiles = ArrayList<SoundFile>()
soundFiles.add(SoundFile(soundTheme, type))
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
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.preference.PreferenceManager
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.extensions.withImmutableFlag
import com.habitrpg.android.habitica.models.tasks.RemindersItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.android.habitica.receivers.NotificationPublisher
import com.habitrpg.android.habitica.receivers.TaskReceiver
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.shared.habitica.HLogger
import com.habitrpg.shared.habitica.LogLevel
import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import java.time.DateTimeException
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
class TaskAlarmManager(
private var context: Context,
private var taskRepository: TaskRepository,
private var authenticationHandler: AuthenticationHandler
) {
private val am: AlarmManager? = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
private val upcomingReminderOccurrencesToSchedule = 3
/**
* 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.
* 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.
*
* 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
* 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
* (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
* is processed to schedule the upcoming alarms.
*/
private fun setAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch {
val reminderOccurencesToSchedule = if (task.type == TaskType.TODO) { 1 } else {
// For dailies, we schedule multiple reminders in advance
upcomingReminderOccurrencesToSchedule
}
task.reminders?.let { reminders ->
for (reminder in reminders) {
try {
val upcomingReminders =
task.getNextReminderOccurrences(reminder, reminderOccurencesToSchedule)
upcomingReminders?.forEachIndexed { index, reminderNextOccurrenceTime ->
reminder?.time =
reminderNextOccurrenceTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
setAlarmForRemindersItem(task, reminder, index)
}
} catch (_: DateTimeException) {
// code accidentally generated an invalid date
}
}
}
}
}
fun removeAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch {
task.reminders?.let { reminders ->
// Remove not only the immediate reminder, but also the next however many (upcomingReminderOccurrencesToSchedule) reminders
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
// We may be able to use repeating alarms instead of this in the future
fun addAlarmForTaskId(taskId: String) {
MainScope().launch(ExceptionHandler.coroutine()) {
val task = taskRepository.getTaskCopy(taskId)
.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) }
if (!preventDailyReminder) {
scheduleDailyReminder(context)
}
}
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.
* 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
* and its scheduled time. This unique identifier ensures that each reminder occurrence is distinctly handled.
*
* If an alarm with the same identifier already exists, it is cancelled and replaced with the new one.
* This ensures that reminders are always up to date with their latest scheduled times.
*
* The alarm is scheduled to trigger at the exact time specified in the reminder. Upon triggering,
* it will send a broadcast to `TaskReceiver`, which should handle the reminder notification.
*
* @param reminderItemTask The task associated with the reminder.
* @param remindersItem The reminder item containing details like ID and the time for the reminder.
* If this is null, the method returns immediately without scheduling an alarm.
*/
private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?, occurrenceIndex: Int) {
if (remindersItem == null) return
val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant()
val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant()
if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) {
return
}
val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id
intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text)
intent.putExtra(TASK_ID_INTENT_KEY, reminderItemTask.id)
// Create a unique identifier based on remindersItem.id and the occurrence index
val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex
// Cancel alarm if already exists
val previousSender = PendingIntent.getBroadcast(
context,
intentId,
intent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE)
)
if (previousSender != null) {
previousSender.cancel()
am?.cancel(previousSender)
}
val sender = PendingIntent.getBroadcast(
context,
intentId,
intent,
withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT)
)
CoroutineScope(Dispatchers.IO).launch {
setAlarm(context, reminderZonedTime.toEpochMilli(), sender)
}
}
private fun removeAlarmForRemindersItem(remindersItem: RemindersItem, occurrenceIndex: Int? = null) {
val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id
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,
intent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
)
val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
sender.cancel()
am?.cancel(sender)
}
companion object {
const val TASK_ID_INTENT_KEY = "TASK_ID"
const val TASK_NAME_INTENT_KEY = "TASK_NAME"
fun scheduleDailyReminder(context: Context?) {
if (context == null) return
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getBoolean("use_reminder", false)) {
val timeval = prefs.getString("reminder_time", "19:00")
val pieces =
timeval?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
?: return
val hour = Integer.parseInt(pieces[0])
val minute = Integer.parseInt(pieces[1])
val cal = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, hour)
cal.set(Calendar.MINUTE, minute)
cal.set(Calendar.SECOND, 0)
if (cal.timeInMillis < Date().time) {
cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) + 1)
}
val triggerTime = cal.timeInMillis
val notificationIntent = Intent(context, NotificationPublisher::class.java)
notificationIntent.putExtra(NotificationPublisher.NOTIFICATION_ID, 1)
notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val previousSender = PendingIntent.getBroadcast(
context,
0,
notificationIntent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE)
)
if (previousSender != null) {
previousSender.cancel()
alarmManager?.cancel(previousSender)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
notificationIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
)
setAlarm(context, triggerTime, pendingIntent)
}
}
fun removeDailyReminder(context: Context?) {
val notificationIntent = Intent(context, NotificationPublisher::class.java)
val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val displayIntent =
PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(0))
alarmManager?.cancel(displayIntent)
}
private fun setAlarm(context: Context, 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)
}
}
}
}
package com.habitrpg.android.habitica.helpers
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.preference.PreferenceManager
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.extensions.withImmutableFlag
import com.habitrpg.android.habitica.models.tasks.RemindersItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.android.habitica.receivers.NotificationPublisher
import com.habitrpg.android.habitica.receivers.TaskReceiver
import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.shared.habitica.HLogger
import com.habitrpg.shared.habitica.LogLevel
import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import java.time.DateTimeException
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
class TaskAlarmManager(
private var context: Context,
private var taskRepository: TaskRepository,
private var authenticationHandler: AuthenticationHandler,
) {
private val am: AlarmManager? = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
private val upcomingReminderOccurrencesToSchedule = 3
/**
* 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.
* 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.
*
* 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
* 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
* (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
* is processed to schedule the upcoming alarms.
*/
private fun setAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch {
val reminderOccurencesToSchedule =
if (task.type == TaskType.TODO) {
1
} else {
// For dailies, we schedule multiple reminders in advance
upcomingReminderOccurrencesToSchedule
}
task.reminders?.let { reminders ->
for (reminder in reminders) {
try {
val upcomingReminders =
task.getNextReminderOccurrences(reminder, reminderOccurencesToSchedule)
upcomingReminders?.forEachIndexed { index, reminderNextOccurrenceTime ->
reminder?.time =
reminderNextOccurrenceTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
setAlarmForRemindersItem(task, reminder, index)
}
} catch (_: DateTimeException) {
// code accidentally generated an invalid date
}
}
}
}
}
fun removeAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch {
task.reminders?.let { reminders ->
// Remove not only the immediate reminder, but also the next however many (upcomingReminderOccurrencesToSchedule) reminders
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
// We may be able to use repeating alarms instead of this in the future
fun addAlarmForTaskId(taskId: String) {
MainScope().launch(ExceptionHandler.coroutine()) {
val task =
taskRepository.getTaskCopy(taskId)
.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) }
if (!preventDailyReminder) {
scheduleDailyReminder(context)
}
}
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.
* 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
* and its scheduled time. This unique identifier ensures that each reminder occurrence is distinctly handled.
*
* If an alarm with the same identifier already exists, it is cancelled and replaced with the new one.
* This ensures that reminders are always up to date with their latest scheduled times.
*
* The alarm is scheduled to trigger at the exact time specified in the reminder. Upon triggering,
* it will send a broadcast to `TaskReceiver`, which should handle the reminder notification.
*
* @param reminderItemTask The task associated with the reminder.
* @param remindersItem The reminder item containing details like ID and the time for the reminder.
* If this is null, the method returns immediately without scheduling an alarm.
*/
private fun setAlarmForRemindersItem(
reminderItemTask: Task,
remindersItem: RemindersItem?,
occurrenceIndex: Int,
) {
if (remindersItem == null) return
val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant()
val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant()
if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) {
return
}
val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id
intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text)
intent.putExtra(TASK_ID_INTENT_KEY, reminderItemTask.id)
// Create a unique identifier based on remindersItem.id and the occurrence index
val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex
// Cancel alarm if already exists
val previousSender =
PendingIntent.getBroadcast(
context,
intentId,
intent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE),
)
if (previousSender != null) {
previousSender.cancel()
am?.cancel(previousSender)
}
val sender =
PendingIntent.getBroadcast(
context,
intentId,
intent,
withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT),
)
CoroutineScope(Dispatchers.IO).launch {
setAlarm(context, reminderZonedTime.toEpochMilli(), sender)
}
}
private fun removeAlarmForRemindersItem(
remindersItem: RemindersItem,
occurrenceIndex: Int? = null,
) {
val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id
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,
intent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
sender.cancel()
am?.cancel(sender)
}
companion object {
const val TASK_ID_INTENT_KEY = "TASK_ID"
const val TASK_NAME_INTENT_KEY = "TASK_NAME"
fun scheduleDailyReminder(context: Context?) {
if (context == null) return
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getBoolean("use_reminder", false)) {
val timeval = prefs.getString("reminder_time", "19:00")
val pieces =
timeval?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
?: return
val hour = Integer.parseInt(pieces[0])
val minute = Integer.parseInt(pieces[1])
val cal = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, hour)
cal.set(Calendar.MINUTE, minute)
cal.set(Calendar.SECOND, 0)
if (cal.timeInMillis < Date().time) {
cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) + 1)
}
val triggerTime = cal.timeInMillis
val notificationIntent = Intent(context, NotificationPublisher::class.java)
notificationIntent.putExtra(NotificationPublisher.NOTIFICATION_ID, 1)
notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val previousSender =
PendingIntent.getBroadcast(
context,
0,
notificationIntent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE),
)
if (previousSender != null) {
previousSender.cancel()
alarmManager?.cancel(previousSender)
}
val pendingIntent =
PendingIntent.getBroadcast(
context,
0,
notificationIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
setAlarm(context, triggerTime, pendingIntent)
}
}
fun removeDailyReminder(context: Context?) {
val notificationIntent = Intent(context, NotificationPublisher::class.java)
val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val displayIntent =
PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(0))
alarmManager?.cancel(displayIntent)
}
private fun setAlarm(
context: Context,
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
class TaskDescriptionBuilder(private val context: Context) {
fun describe(task: Task): String {
return when (task.type) {
TaskType.HABIT -> context.getString(
R.string.habit_summary_description,
describeHabitDirections(task.up ?: false, task.down ?: false),
describeDifficulty(task.priority)
)
TaskType.HABIT ->
context.getString(
R.string.habit_summary_description,
describeHabitDirections(task.up ?: false, task.down ?: false),
describeDifficulty(task.priority),
)
TaskType.TODO -> {
if (task.dueDate != null) {
context.getString(
R.string.todo_summary_description_duedate,
describeDifficulty(task.priority),
describeDate(task.dueDate!!)
describeDate(task.dueDate!!),
)
} else {
context.getString(
R.string.todo_summary_description,
describeDifficulty(task.priority)
describeDifficulty(task.priority),
)
}
}
TaskType.DAILY -> context.getString(
R.string.daily_summary_description,
describeDifficulty(task.priority),
describeRepeatInterval(task.frequency, task.everyX ?: 1),
describeRepeatDays(task)
)
TaskType.DAILY ->
context.getString(
R.string.daily_summary_description,
describeDifficulty(task.priority),
describeRepeatInterval(task.frequency, task.everyX ?: 1),
describeRepeatDays(task),
)
else -> ""
}
}
@ -59,48 +63,61 @@ class TaskDescriptionBuilder(private val context: Context) {
}
return when (task.frequency) {
Frequency.WEEKLY -> {
" " + if (task.repeat?.isEveryDay == true) {
context.getString(R.string.on_every_day_of_week)
} else {
if (task.repeat?.isOnlyWeekdays == true) {
context.getString(R.string.on_weekdays)
} else if (task.repeat?.isOnlyWeekends == true) {
context.getString(R.string.on_weekends)
" " +
if (task.repeat?.isEveryDay == true) {
context.getString(R.string.on_every_day_of_week)
} else {
val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
joinToCount(dayStrings)
if (task.repeat?.isOnlyWeekdays == true) {
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))
} else {
""
}
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),
)
} else {
""
}
}
Frequency.YEARLY -> " " + context.getString(
R.string.on_x,
task.startDate?.let {
val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR
DateUtils.formatDateTime(context, it.time, flags)
} ?: ""
)
Frequency.YEARLY ->
" " +
context.getString(
R.string.on_x,
task.startDate?.let {
val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR
DateUtils.formatDateTime(context, it.time, flags)
} ?: "",
)
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) {
return context.getString(R.string.never)
}
return when (interval) {
Frequency.DAILY -> context.resources.getQuantityString(R.plurals.repeat_daily, everyX, everyX)
Frequency.WEEKLY -> context.resources.getQuantityString(R.plurals.repeat_weekly, everyX, everyX)
Frequency.MONTHLY -> context.resources.getQuantityString(
R.plurals.repeat_monthly,
everyX,
everyX
)
Frequency.YEARLY -> context.resources.getQuantityString(R.plurals.repeat_yearly, everyX, everyX)
Frequency.DAILY ->
context.resources.getQuantityString(
R.plurals.repeat_daily,
everyX,
everyX,
)
Frequency.WEEKLY ->
context.resources.getQuantityString(
R.plurals.repeat_weekly,
everyX,
everyX,
)
Frequency.MONTHLY ->
context.resources.getQuantityString(
R.plurals.repeat_monthly,
everyX,
everyX,
)
Frequency.YEARLY ->
context.resources.getQuantityString(
R.plurals.repeat_yearly,
everyX,
everyX,
)
null -> ""
}
}
private fun describeHabitDirections(up: Boolean, down: Boolean): String {
private fun describeHabitDirections(
up: Boolean,
down: Boolean,
): String {
return if (up && down) {
context.getString(R.string.positive_and_negative)
} else if (up) {

View file

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

View file

@ -2,4 +2,5 @@ package com.habitrpg.android.habitica.helpers.notifications
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
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.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 {
return data["groupID"].hashCode()
}
@ -27,17 +27,22 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
override fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder {
val user = Person.Builder().setName("You").build()
val message = makeMessageFromData(data)
var style = NotificationCompat.MessagingStyle(user)
.setGroupConversation(true)
.setConversationTitle(data["groupName"])
var style =
NotificationCompat.MessagingStyle(user)
.setGroupConversation(true)
.setConversationTitle(data["groupName"])
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
val existingNotifications = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notificationManager?.activeNotifications?.filter { it.id == getNotificationID(data) }
} else {
null
}
val oldMessages = existingNotifications?.firstOrNull()?.notification?.extras?.getBundle("messages")?.get("messages") as? ArrayList<Map<String, String>> ?: arrayListOf()
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
val existingNotifications =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notificationManager?.activeNotifications?.filter { it.id == getNotificationID(data) }
} else {
null
}
val oldMessages =
existingNotifications?.firstOrNull()?.notification?.extras?.getBundle("messages")
?.get("messages") as? ArrayList<Map<String, String>> ?: arrayListOf()
for (oldMessage in oldMessages) {
style = style.addMessage(makeMessageFromData(oldMessage))
}
@ -57,20 +62,24 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
return NotificationCompat.MessagingStyle.Message(
messageText,
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)
val groupID = data["groupID"] ?: return
val actionName = context.getString(R.string.group_message_reply)
val replyLabel: String = context.getString(R.string.reply)
val remoteInput: RemoteInput = RemoteInput.Builder(actionName).run {
setLabel(replyLabel)
build()
}
val remoteInput: RemoteInput =
RemoteInput.Builder(actionName).run {
setLabel(replyLabel)
build()
}
val intent = Intent(context, LocalNotificationActionReceiver::class.java)
intent.action = actionName
intent.putExtra("groupID", groupID)
@ -80,14 +89,14 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
context,
groupID.hashCode(),
intent,
withMutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
withMutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
val action: NotificationCompat.Action =
NotificationCompat.Action.Builder(
R.drawable.ic_send_grey_600_24dp,
context.getString(R.string.reply),
replyPendingIntent
replyPendingIntent,
)
.addRemoteInput(remoteInput)
.build()

View file

@ -10,14 +10,17 @@ import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver
/**
* 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) {
super.configureMainIntent(intent)
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)
val res = context.resources
@ -26,24 +29,26 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi
val groupID = data["groupID"]
acceptInviteIntent.putExtra("groupID", groupID)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentAccept = PendingIntent.getBroadcast(
context,
groupID.hashCode(),
acceptInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
)
val pendingIntentAccept =
PendingIntent.getBroadcast(
context,
groupID.hashCode(),
acceptInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
notificationBuilder.addAction(0, "Accept", pendingIntentAccept)
val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
rejectInviteIntent.action = res.getString(R.string.reject_guild_invite)
rejectInviteIntent.putExtra("groupID", groupID)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentReject = PendingIntent.getBroadcast(
context,
groupID.hashCode() + 1,
rejectInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
)
val pendingIntentReject =
PendingIntent.getBroadcast(
context,
groupID.hashCode() + 1,
rejectInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
notificationBuilder.addAction(0, "Reject", pendingIntentReject)
}
}

View file

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

View file

@ -18,16 +18,16 @@ import java.util.Date
*/
abstract class HabiticaLocalNotification(
protected var context: Context,
protected var identifier: String?
protected var identifier: String?,
) {
protected var data: Map<String, String>? = null
protected var title: String? = null
protected var message: String? = null
protected var notificationBuilder = NotificationCompat.Builder(context, "default")
.setSmallIcon(R.drawable.ic_gryphon_white)
.setAutoCancel(true)
protected var notificationBuilder =
NotificationCompat.Builder(context, "default")
.setSmallIcon(R.drawable.ic_gryphon_white)
.setAutoCancel(true)
open fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder {
val path = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
@ -37,7 +37,11 @@ abstract class HabiticaLocalNotification(
}
@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.message = message
@ -65,16 +69,20 @@ abstract class HabiticaLocalNotification(
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)
configureMainIntent(intent)
intent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntent = PendingIntent.getActivity(
context,
3000,
intent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
)
val pendingIntent =
PendingIntent.getActivity(
context,
3000,
intent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
notificationBuilder.setContentIntent(pendingIntent)
}

View file

@ -4,44 +4,95 @@ import android.content.Context
class HabiticaLocalNotificationFactory {
// 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 {
PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
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)
}
PushNotificationManager.RECEIVED_GEMS_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.RECEIVED_GEMS_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
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)
}
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
GuildInviteLocalNotification(context!!, notificationType)
}
PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
QuestInviteLocalNotification(context!!, notificationType)
}
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
QuestBegunLocalNotification(context!!, notificationType)
}
PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
WonChallengeLocalNotification(context!!, notificationType)
}
PushNotificationManager.CHANGE_USERNAME_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.CHANGE_USERNAME_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
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)
}
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
ChatMentionNotification(context!!, notificationType)
}
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
GroupActivityNotification(context!!, notificationType)
}
else -> {
GenericLocalNotification(context!!, notificationType)
}

View file

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

View file

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

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