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

@ -48,14 +48,16 @@ import java.lang.ref.WeakReference
import java.util.Date
import javax.inject.Inject
class ApplicationLifecycleTracker(private val sharedPreferences: SharedPreferences): DefaultLifecycleObserver {
class ApplicationLifecycleTracker(private val sharedPreferences: SharedPreferences) :
DefaultLifecycleObserver {
private var lastResumeTime = 0L
override fun onResume(owner : LifecycleOwner) {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
lastResumeTime = Date().time
}
override fun onPause(owner : LifecycleOwner) {
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
val duration = Date().time - lastResumeTime
addDurationToDay(duration / 1000)
@ -107,9 +109,6 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
private lateinit var lifecycleTracker: ApplicationLifecycleTracker
/**
* For better performance billing class should be used as singleton
*/
// endregion
override fun onCreate() {
@ -148,7 +147,6 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
checkIfNewVersion()
}
private fun setupAdHandler() {
AdHandler.setup(sharedPrefs)
}
@ -158,8 +156,8 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
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 {
configuration.locales.isEmpty || configuration.locales[0] != languageHelper.locale
} else {
@Suppress("DEPRECATION")
configuration.locale != languageHelper.locale
}
@ -171,15 +169,16 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
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
}
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) {
@ -212,7 +211,7 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
override fun openOrCreateDatabase(
name: String,
mode: Int,
factory: SQLiteDatabase.CursorFactory?
factory: SQLiteDatabase.CursorFactory?,
): SQLiteDatabase {
return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory)
}
@ -221,9 +220,14 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
name: String,
mode: Int,
factory: SQLiteDatabase.CursorFactory?,
errorHandler: DatabaseErrorHandler?
errorHandler: DatabaseErrorHandler?,
): SQLiteDatabase {
return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory, errorHandler)
return super.openOrCreateDatabase(
getDatabasePath(name).absolutePath,
mode,
factory,
errorHandler,
)
}
// endregion
@ -241,9 +245,10 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
private fun setupRemoteConfig() {
val remoteConfig = FirebaseRemoteConfig.getInstance()
val configSettings = FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600)
.build()
val configSettings =
FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600)
.build()
remoteConfig.setConfigSettingsAsync(configSettings)
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
remoteConfig.fetchAndActivate()
@ -278,13 +283,19 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
}
}
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
override fun onActivityCreated(
p0: Activity,
p1: Bundle?,
) {
}
override fun onActivityDestroyed(p0: Activity) {
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {
override fun onActivitySaveInstanceState(
p0: Activity,
p1: Bundle,
) {
}
override fun onActivityStopped(p0: Activity) {
@ -319,7 +330,10 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
}
}
private fun startActivity(activityClass: Class<*>, context: Context) {
private fun startActivity(
activityClass: Class<*>,
context: Context,
) {
val intent = Intent(context, activityClass)
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

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

View file

@ -4,5 +4,9 @@ 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>>
fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>>
}

View file

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

View file

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

View file

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

View file

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

View file

@ -4,15 +4,19 @@ import com.habitrpg.android.habitica.models.Tag
import kotlinx.coroutines.flow.Flow
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

@ -12,59 +12,124 @@ 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)
fun getTasks(
taskType: TaskType,
userID: String? = null,
includedGroupIDs: Array<String>,
): Flow<List<Task>>
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList?
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): TaskList?
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)?
notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult?
suspend fun taskChecked(
user: User?,
taskId: String,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult?
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
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 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 markTaskCompleted(
taskId: String,
isCompleted: Boolean,
)
fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit)
fun <T : BaseMainObject> modify(
obj: T,
transaction: (T) -> Unit,
)
fun swapTaskPosition(firstPosition: Int, secondPosition: Int)
fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
)
suspend fun updateTaskPosition(taskType: TaskType, taskID: String, newPosition: Int): List<String>?
suspend fun updateTaskPosition(
taskType: TaskType,
taskID: String,
newPosition: Int,
): List<String>?
fun getUnmanagedTask(taskid: String): Flow<Task>
fun updateTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>)
fun updateTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
)
fun createTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>)
fun createTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
)
fun getTaskCopies(): Flow<List<Task>>
fun getTaskCopies(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?
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)
suspend fun markTaskNeedsWork(
task: Task,
userID: String,
)
}

View file

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

View file

@ -20,12 +20,21 @@ 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 updateUser(
key: String,
value: Any?,
): User?
suspend fun retrieveUser(
withTasks: Boolean = false,
forced: Boolean = false,
overrideExisting: Boolean = false,
): User?
suspend fun revive(): Equipment?
@ -37,23 +46,40 @@ interface UserRepository : BaseRepository {
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 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(
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?
@ -61,29 +87,61 @@ interface UserRepository : BaseRepository {
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 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 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

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

View file

@ -10,10 +10,18 @@ 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>> {
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

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

View file

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

View file

@ -8,82 +8,205 @@ import com.habitrpg.android.habitica.models.user.User
import javax.inject.Inject
@Suppress("StringLiteralDuplication")
class SetupCustomizationRepositoryImpl @Inject
constructor(private val context: Context) : SetupCustomizationRepository {
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 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 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 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 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 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),
)
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, 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",
)
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()
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_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()
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
}
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

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

View file

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

View file

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

View file

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

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

@ -4,5 +4,9 @@ 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>>
fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>>
}

View file

@ -14,8 +14,8 @@ import com.habitrpg.android.habitica.models.user.User
import kotlinx.coroutines.flow.Flow
interface InventoryLocalRepository : ContentLocalRepository {
fun getArmoireRemainingCount(): Flow<Int>
fun getOwnedEquipment(): Flow<List<Equipment>>
fun getMounts(): Flow<List<Mount>>
@ -27,44 +27,116 @@ interface InventoryLocalRepository : ContentLocalRepository {
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 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 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?)
suspend fun changeOwnedCount(
type: String,
key: String,
userID: String,
amountToAdd: Int,
)
fun getItem(type: String, key: String): Flow<Item>
fun getOwnedItem(userID: String, type: String, key: String, includeZero: Boolean): Flow<OwnedItem>
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 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 soldItem(
userID: String,
updatedUser: User,
): User
fun getAvailableLimitedItems(): Flow<List<Item>>
fun save(items: Items, userID: String)
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>>
fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>>
}

View file

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

View file

@ -8,28 +8,56 @@ 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(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 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 markTaskCompleted(
taskId: String,
isCompleted: Boolean,
)
fun swapTaskPosition(firstPosition: Int, secondPosition: Int)
fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
)
fun getTaskAtPosition(taskType: String, position: Int): Flow<Task>
fun getTaskAtPosition(
taskType: String,
position: Int,
): Flow<Task>
fun updateIsdue(daily: TaskList): TaskList
fun updateTaskPositions(taskOrder: List<String>)
fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>)
fun saveCompletedTodos(
userId: String,
tasks: MutableCollection<Task>,
)
fun getErroredTasks(userID: String): Flow<List<Task>>
fun getUser(userID: String): Flow<User>
fun getTasksForChallenge(challengeID: String?, userID: String?): Flow<List<Task>>
fun getTasksForChallenge(
challengeID: String?,
userID: String?,
): Flow<List<Task>>
}

View file

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

View file

@ -13,20 +13,28 @@ 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 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

@ -14,115 +14,131 @@ 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 {
abstract class RealmBaseLocalRepository internal constructor(override var realm: Realm) :
BaseLocalRepository {
override val isClosed: Boolean
get() = realm.isClosed
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 close() {
realm.close()
}
}
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)
}
override fun executeTransaction(transaction: (Realm) -> Unit) {
pendingSaves.add(transaction)
if (isSaving.compareAndSet(false, true)) {
process()
}
isSaving.set(false)
}
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() }
}
}
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

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

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

@ -35,12 +35,11 @@ import java.util.Date
class TaskAlarmManager(
private var context: Context,
private var taskRepository: TaskRepository,
private var authenticationHandler: AuthenticationHandler
private var authenticationHandler: AuthenticationHandler,
) {
private val am: AlarmManager? = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
private val upcomingReminderOccurrencesToSchedule = 3
/**
* Schedules multiple alarms for each reminder associated with a given task.
*
@ -60,10 +59,13 @@ class TaskAlarmManager(
*/
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
}
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 {
@ -82,7 +84,6 @@ class TaskAlarmManager(
}
}
fun removeAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch {
task.reminders?.let { reminders ->
@ -99,9 +100,10 @@ class TaskAlarmManager(
// 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()
val task =
taskRepository.getTaskCopy(taskId)
.filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type }
.first()
setAlarmsForTask(task)
}
}
@ -137,7 +139,11 @@ class TaskAlarmManager(
* @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) {
private fun setAlarmForRemindersItem(
reminderItemTask: Task,
remindersItem: RemindersItem?,
occurrenceIndex: Int,
) {
if (remindersItem == null) return
val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant()
@ -147,7 +153,6 @@ class TaskAlarmManager(
return
}
val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id
intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text)
@ -157,40 +162,56 @@ class TaskAlarmManager(
val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex
// Cancel alarm if already exists
val previousSender = PendingIntent.getBroadcast(
context,
intentId,
intent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE)
)
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)
)
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) {
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 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)
@ -225,23 +246,25 @@ class TaskAlarmManager(
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)
)
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)
)
val pendingIntent =
PendingIntent.getBroadcast(
context,
0,
notificationIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
setAlarm(context, triggerTime, pendingIntent)
}
@ -255,7 +278,11 @@ class TaskAlarmManager(
alarmManager?.cancel(displayIntent)
}
private fun setAlarm(context: Context, time: Long, pendingIntent: PendingIntent?) {
private fun setAlarm(
context: Context,
time: Long,
pendingIntent: PendingIntent?,
) {
HLogger.log(LogLevel.INFO, "TaskAlarmManager", "Scheduling for $time")
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
@ -267,16 +294,24 @@ class TaskAlarmManager(
// 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")
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)
alarmManager?.setWindow(
AlarmManager.RTC_WAKEUP,
time,
600000,
pendingIntent,
)
}
else -> {
throw ex
}
}
}
} else {

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

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

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

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

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

@ -18,9 +18,8 @@ import java.io.IOException
class PushNotificationManager(
var apiClient: ApiClient,
private val sharedPreferences: SharedPreferences,
private val context: Context
private val context: Context,
) {
var refreshedToken: String = ""
set(value) {
if (value.isEmpty()) {
@ -100,18 +99,19 @@ class PushNotificationManager(
}
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
}
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)
}
@ -133,7 +133,11 @@ class PushNotificationManager(
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) {
fun displayNotification(
remoteMessage: RemoteMessage,
context: Context,
pushNotificationManager: PushNotificationManager? = null,
) {
val remoteMessageIdentifier = remoteMessage.data["identifier"]
if (pushNotificationManager?.userIsSubscribedToNotificationType(remoteMessageIdentifier) != false) {
@ -144,28 +148,29 @@ class PushNotificationManager(
"receive notification",
EventCategory.BEHAVIOUR,
HitType.EVENT,
additionalData
additionalData,
)
}
val notificationFactory = HabiticaLocalNotificationFactory()
val localNotification = notificationFactory.build(
remoteMessageIdentifier,
context
)
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
remoteMessage.data,
)
} else {
localNotification.notifyLocally(
remoteMessage.data["title"],
remoteMessage.data["body"],
remoteMessage.data
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