Merge branch 'version/4.1' into Hafiz/change-class-update

This commit is contained in:
Phillip Thelen 2022-11-15 14:19:40 +01:00 committed by GitHub
commit 9542d24d2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
267 changed files with 3987 additions and 2957 deletions

View file

@ -83,7 +83,7 @@ GEM
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-properties (1.1.2)
java-properties
fastlane-plugin-semantic_release (1.14.1)
fastlane-plugin-semantic_release (1.18.0)
fastlane-plugin-versioning_android (0.1.0)
gh_inspector (1.1.3)
google-api-client (0.38.0)

View file

@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
<application
android:name=".HabiticaApplication"
@ -29,7 +30,6 @@
android:value="@string/application_ad_id"/>
<activity
android:name=".ui.activities.MainActivity"
android:label="@string/app_name"
android:theme="@style/LaunchAppTheme"
android:windowSoftInputMode="stateHidden|adjustResize"
android:configChanges="screenSize | smallestScreenSize | screenLayout | orientation"
@ -256,6 +256,12 @@
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_gryphon_white" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/brand_300" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/content_provider"

View file

@ -43,7 +43,7 @@ dependencies {
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
//App Compatibility and Material Design
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.android.material:material:1.7.0'
implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
implementation "androidx.preference:preference-ktx:$preferences_version"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
@ -79,7 +79,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
debugImplementation 'androidx.fragment:fragment-testing:1.5.2'
debugImplementation 'androidx.fragment:fragment-testing:1.5.4'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
@ -98,7 +98,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-config-ktx'
implementation 'com.google.firebase:firebase-perf-ktx'
implementation 'com.google.android.gms:play-services-ads:21.1.0'
implementation 'com.google.android.gms:play-services-ads:21.3.0'
implementation "com.google.android.gms:play-services-auth:$play_auth_version"
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation "com.google.android.gms:play-services-wearable:$play_wearables_version"
@ -107,12 +107,20 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
implementation "androidx.fragment:fragment-ktx:1.5.4"
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "com.google.android.material:compose-theme-adapter:1.1.21"
implementation 'androidx.activity:activity-compose:1.6.1'
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
implementation 'com.willowtreeapps:signinwithapplebutton:0.3'
@ -139,7 +147,7 @@ android {
}
defaultConfig {
minSdkVersion 21
minSdkVersion min_sdk
applicationId "com.habitrpg.android.habitica"
vectorDrawables.useSupportLibrary = true
buildConfigField "String", "STORE", "\"google\""
@ -160,6 +168,11 @@ android {
buildFeatures {
viewBinding true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.2"
}
signingConfigs {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View file

@ -28,16 +28,15 @@
app:expandedTitleMarginStart="0dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include
android:id="@+id/avatar_with_bars"
layout="@layout/avatar_with_bars"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/header_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginEnd="@dimen/header_border_spacing"
android:layout_marginStart="@dimen/header_border_spacing"
android:layout_marginBottom="@dimen/spacing_medium"
app:layout_collapseMode="parallax" />
app:layout_collapseMode="parallax"/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"

View file

@ -179,6 +179,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="11dp"
android:lineSpacingExtra="4sp"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"
tools:text="Theres always more to learn! Maybe its a specialized topic youre interested in, or maybe its the experiences of a notable contributor. Read about it and you may win 15 gems!" />
</com.habitrpg.android.habitica.ui.views.CollapsibleSectionView>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>

View file

@ -17,6 +17,7 @@
android:background="@drawable/g1g1_box"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="@dimen/spacing_large"
android:clipChildren="true"
android:clipToPadding="true"
android:clickable="true"
@ -29,14 +30,16 @@
android:scaleType="center"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="false"/>
android:layout_alignParentBottom="false"
android:importantForAccessibility="no"/>
<ImageView
android:id="@+id/promo_banner_right_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="center"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"/>
android:layout_alignParentEnd="true"
android:importantForAccessibility="no"/>
<LinearLayout
android:layout_centerInParent="true"
android:layout_width="wrap_content"

View file

@ -5,7 +5,7 @@
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/refreshLayout"
android:background="@color/window_background">
android:background="@color/content_background">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -101,10 +101,6 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/colorContentBackground">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/content_background_offset" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -130,11 +126,6 @@
style="@style/HabiticaButton.Green"
android:layout_marginStart="@dimen/spacing_large"/>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/content_background_offset" />
<LinearLayout
android:id="@+id/leader_wrapper"
android:layout_width="match_parent"
@ -183,7 +174,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/inn_description"
android:layout_marginTop="@dimen/spacing_large" />
android:layout_marginTop="@dimen/spacing_large"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"/>
</com.habitrpg.android.habitica.ui.views.CollapsibleSectionView>
<com.habitrpg.android.habitica.ui.views.CollapsibleSectionView
android:layout_width="match_parent"
@ -195,14 +188,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/inn_description"
android:layout_marginTop="@dimen/spacing_large" />
android:layout_marginTop="@dimen/spacing_large"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large" />
</com.habitrpg.android.habitica.ui.views.CollapsibleSectionView>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/content_background_offset"
android:layout_marginTop="@dimen/spacing_medium"/>
</LinearLayout>
<Button
android:id="@+id/leave_button"

View file

@ -38,9 +38,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/colorContentBackground"
android:showDividers="beginning|end|middle"
android:divider="@drawable/vertical_divider">
android:background="?attr/colorContentBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -180,7 +178,9 @@
android:id="@+id/description_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="This is our super awesome party!"/>
tools:text="This is our super awesome party!"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"

View file

@ -43,7 +43,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/inn_description"
android:layout_marginTop="@dimen/spacing_large"/>
android:layout_marginTop="@dimen/spacing_large"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"/>
</com.habitrpg.android.habitica.ui.views.CollapsibleSectionView>
<com.habitrpg.android.habitica.ui.views.CollapsibleSectionView
android:layout_width="match_parent"
@ -61,7 +63,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/guidelines_description"
android:layout_marginTop="@dimen/spacing_large"/>
android:layout_marginTop="@dimen/spacing_large"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"/>
</com.habitrpg.android.habitica.ui.views.CollapsibleSectionView>
<com.habitrpg.android.habitica.ui.views.CollapsibleSectionView
android:layout_width="match_parent"
@ -100,7 +104,9 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tiers_descriptions"/>
android:text="@string/tiers_descriptions"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"/>
</com.habitrpg.android.habitica.ui.views.CollapsibleSectionView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -5,9 +5,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:background="@drawable/layout_rounded_bg_window"
android:padding="@dimen/spacing_large"
android:foreground="?android:attr/selectableItemBackground"
android:focusable="true">
android:focusable="true"
android:layout_marginBottom="@dimen/spacing_medium">
<com.habitrpg.common.habitica.views.AvatarView
android:id="@+id/avatarView"
android:layout_width="97dp"
@ -88,7 +90,7 @@
android:layout_height="@dimen/bar_size"
android:id="@+id/health_bar"
app:barForegroundColor="@color/hpColor"
app:barBackgroundColor="@color/window_background"/>
app:barBackgroundColor="@color/offset_background"/>
<TextView
android:id="@+id/health_textview"
android:layout_width="wrap_content"
@ -106,7 +108,7 @@
android:layout_height="@dimen/bar_size"
android:id="@+id/experience_bar"
app:barForegroundColor="@color/xpColor"
app:barBackgroundColor="@color/window_background"/>
app:barBackgroundColor="@color/offset_background"/>
<TextView
android:id="@+id/experience_textview"
android:layout_width="wrap_content"
@ -124,7 +126,7 @@
android:layout_height="@dimen/bar_size"
android:id="@+id/mana_bar"
app:barForegroundColor="@color/mpColor"
app:barBackgroundColor="@color/window_background"/>
app:barBackgroundColor="@color/offset_background"/>
<TextView
android:id="@+id/mana_textview"
android:layout_width="wrap_content"

View file

@ -17,10 +17,6 @@
android:contentDescription="@string/gems"
android:layout_marginTop="20dp"/>
<com.habitrpg.android.habitica.ui.views.SparkView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -40,6 +36,7 @@
android:layout_marginEnd="2dp"
tools:text="21" />
<com.habitrpg.android.habitica.ui.views.DayNightTextView
android:id="@+id/gem_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:dayTextColor="@color/white"

View file

@ -49,7 +49,7 @@
android:paddingBottom="2dp"/>
<TextView
android:id="@+id/priceLabel"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/yellow_10"
android:gravity="center_horizontal"

View file

@ -12,6 +12,17 @@
android:paddingBottom="@dimen/task_top_bottom_padding"
android:layout_marginEnd="@dimen/task_text_padding"
android:layout_marginStart="@dimen/task_text_padding">
<TextView
android:id="@+id/assigned_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Caption4"
android:text="@string/pending_approval"
android:textColor="@color/text_ternary"
android:drawableStart="@drawable/assign"
android:drawablePadding="@dimen/spacing_small"
android:layout_marginBottom="2dp"
/>
<com.habitrpg.android.habitica.ui.views.EllipsisTextView
android:id="@+id/checkedTextView"
style="@style/Subheader3"

View file

@ -5,11 +5,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.LinearLayout">
<View
android:id="@+id/separator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/content_background_offset"/>
<LinearLayout
android:id="@+id/section_title_view"
android:layout_width="match_parent"

View file

@ -2,10 +2,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.habitrpg.android.habitica.TaskActivity">
<item android:id="@+id/action_team_info"
android:title="@string/team_information"
android:icon="@drawable/team_info_icon"
app:showAsAction="collapseActionView|always" />
<item android:id="@+id/action_search"
android:title="@string/search"
android:icon="@drawable/ic_search"

View file

@ -4,16 +4,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_nav"
app:startDestination="@id/tasksFragment">
<fragment
android:id="@+id/equipmentOverviewFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.equipment.EquipmentOverviewFragment"
android:label="@string/sidebar_equipment" >
<deepLink app:uri="habitica.com/inventory/equipment" />
<action
android:id="@+id/openEquipmentDetail"
app:destination="@id/equipmentDetailFragment" />
</fragment>
<fragment
android:id="@+id/equipmentDetailFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.equipment.EquipmentDetailFragment"
@ -44,6 +34,16 @@
<deepLink app:uri="habitica.com/tasks" />
<deepLink app:uri="habitica.com" />
</fragment>
<activity
android:id="@+id/taskFormActivity"
android:name="com.habitrpg.android.habitica.ui.activities.TaskFormActivity">
</activity>
<activity
android:id="@+id/taskSummaryActivity"
android:name="com.habitrpg.android.habitica.ui.activities.TaskSummaryActivity">
</activity>
<fragment
android:id="@+id/partyFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.social.party.PartyFragment"
@ -121,8 +121,8 @@
android:id="@+id/openAvatarDetail"
app:destination="@id/avatarCustomizationFragment" />
<action
android:id="@+id/openAvatarEquipment"
app:destination="@id/avatarEquipmentFragment" />
android:id="@+id/openEquipmentDetail"
app:destination="@id/equipmentDetailFragment" />
</fragment>
<fragment
android:id="@+id/itemsFragment"
@ -298,17 +298,6 @@
android:name="category"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/avatarEquipmentFragment"
android:name="com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarEquipmentFragment"
android:label="@string/sidebar_avatar" >
<argument
android:name="type"
app:argType="string" />
<argument
android:name="category"
app:argType="string" />
</fragment>
<activity
android:id="@+id/prefsActivity"
android:name="com.habitrpg.android.habitica.ui.activities.PrefsActivity"

View file

@ -68,4 +68,5 @@
<color name="widget_background">#2B203A</color>
<color name="text_gold">@color/yellow_100</color>
</resources>

View file

@ -119,4 +119,5 @@
<color name="lightly_tinted_background">@color/brand_700</color>
<color name="dialog_background">@color/white</color>
<color name="error_banner_background">@color/maroon_5</color>
<color name="text_gold">@color/yellow_1</color>
</resources>

View file

@ -36,7 +36,7 @@
<dimen name="checkbox_size">24dp</dimen>
<dimen name="checkbox_compact_size">20dp</dimen>
<dimen name="task_top_bottom_padding">10dp</dimen>
<dimen name="task_top_bottom_padding">8dp</dimen>
<dimen name="task_top_bottom_compact_padding">8dp</dimen>
<dimen name="reward_spacing">8dp</dimen>
<dimen name="grid_item_margin">6dp</dimen>

View file

@ -12,6 +12,7 @@
<string name="sidebar_challenges">Challenges</string>
<string name="sidebar_section_inventory">Inventory</string>
<string name="sidebar_avatar">Avatar Customization</string>
<string name="sidebar_avatar_equipment">Avatar &amp; Equipment</string>
<string name="sidebar_equipment">Equipment</string>
<string name="sidebar_stable">Pets &amp; Mounts</string>
<string name="sidebar_news">News</string>

View file

@ -1255,4 +1255,14 @@
<string name="copy_tasks_description">Show assigned and open tasks on your personal task lists</string>
<string name="copy_shared_tasks">Copy shared tasks</string>
<string name="group_plan_settings">Group Plan Settings</string>
<string name="task_summary">Task Summary</string>
<plurals name="you_x_others">
<item quantity="zero">You</item>
<item quantity="one">You, %d other</item>
<item quantity="other">You, %d others</item>
</plurals>
<plurals name="people">
<item quantity="one">%d Person</item>
<item quantity="other">%d People</item>
</plurals>
</resources>

View file

@ -26,6 +26,7 @@
<item name="colorPrimaryDistinct">@color/brand_500</item>
<item name="colorBadgeBackground">@color/brand_400</item>
<item name="taskFormTint">@color/brand_300</item>
<item name="colorSecondaryVariant">@color/brand</item>
<item name="textColorPrimary">@color/text_primary</item>
<item name="textColorSecondary">@color/text_secondary</item>

View file

@ -3,7 +3,6 @@ package com.habitrpg.android.habitica
import android.content.SharedPreferences
import androidx.lifecycle.MutableLiveData
import com.habitrpg.android.habitica.api.GSonFactoryCreator
import com.habitrpg.common.habitica.api.HostConfig
import com.habitrpg.android.habitica.api.MaintenanceApiService
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.ContentRepository
@ -14,9 +13,9 @@ 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.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.SoundManager
import com.habitrpg.android.habitica.interactors.FeedPetUseCase
import com.habitrpg.android.habitica.interactors.HatchPetUseCase
@ -29,22 +28,23 @@ import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.common.habitica.api.HostConfig
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.subjects.PublishSubject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import java.io.InputStreamReader
import java.lang.reflect.Type
import kotlin.reflect.KCallable
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.starProjectedType
import kotlin.reflect.jvm.javaField
import org.junit.Before
open class HabiticaTestCase : TestCase() {
val gson = GSonFactoryCreator.createGson()
@ -68,8 +68,7 @@ open class HabiticaTestCase : TestCase() {
val hatchPetUseCase: HatchPetUseCase = mockk(relaxed = true)
val feedPetUseCase: FeedPetUseCase = mockk(relaxed = true)
val userSubject = PublishSubject.create<User>()
val userEvents: Flowable<User> = userSubject.toFlowable(BackpressureStrategy.DROP)
val userState = MutableStateFlow<User?>(null)
var user = User()
lateinit var content: ContentResult
@ -85,27 +84,27 @@ open class HabiticaTestCase : TestCase() {
user = loadJsonFile("user", User::class.java)
user.stats?.lvl = 20
user.stats?.points = 30
every { userRepository.getUser() } returns userEvents
every { userRepository.getUser() } returns userState
every { userViewModel.user } returns MutableLiveData<User?>(user)
mockkObject(RxErrorHandler)
every { RxErrorHandler.reportError(capture(errorSlot)) } answers {
mockkObject(ExceptionHandler)
every { ExceptionHandler.reportError(capture(errorSlot)) } answers {
throw errorSlot.captured
}
every { socialRepository.getUnmanagedCopy(capture(unmanagedSlot)) } answers { unmanagedSlot.captured }
content = loadJsonFile("content", ContentResult::class.java)
every { inventoryRepository.getPets() } returns Flowable.just(content.pets)
every { inventoryRepository.getMounts() } returns Flowable.just(content.mounts)
every { inventoryRepository.getPets() } returns flowOf(content.pets)
every { inventoryRepository.getMounts() } returns flowOf(content.mounts)
every { inventoryRepository.getItemsFlowable(Food::class.java) } returns Flowable.just(content.food)
every { inventoryRepository.getItemsFlowable(Egg::class.java) } returns Flowable.just(content.eggs)
every { inventoryRepository.getItemsFlowable(HatchingPotion::class.java) } returns Flowable.just(content.hatchingPotions)
every { inventoryRepository.getItemsFlowable(QuestContent::class.java) } returns Flowable.just(content.quests)
every { inventoryRepository.getItemsFlowable(Food::class.java, any()) } returns Flowable.just(content.food)
every { inventoryRepository.getItemsFlowable(Egg::class.java, any()) } answers {
Flowable.just(content.eggs)
every { inventoryRepository.getItems(Food::class.java, any()) } returns flowOf(content.food)
every { inventoryRepository.getItems(Egg::class.java, any()) } answers {
flowOf(content.eggs)
}
every { inventoryRepository.getItemsFlowable(HatchingPotion::class.java, any()) } returns Flowable.just(content.hatchingPotions)
every { inventoryRepository.getItemsFlowable(QuestContent::class.java, any()) } returns Flowable.just(content.quests)
every { inventoryRepository.getItems(HatchingPotion::class.java, any()) } returns flowOf(content.hatchingPotions)
every { inventoryRepository.getItems(QuestContent::class.java, any()) } returns flowOf(content.quests)
}
internal fun <T> loadJsonFile(s: String, type: Type): T {

View file

@ -63,7 +63,7 @@ class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding,
fun setUpUser() {
user.stats?.lvl = 20
user.stats?.points = 30
userSubject.onNext(user)
userState.onNext(user)
every { inventoryRepository.getEquipment(listOf()) } returns Flowable.just(listOf())
}

View file

@ -115,7 +115,7 @@ internal class ItemRecyclerFragmentTest : FragmentTestCase<ItemRecyclerFragment,
items = (items + items).sortedBy { it.key }
Flowable.just(items)
}
every { inventoryRepository.getItemsFlowable(Food::class.java, any()) } answers {
every { inventoryRepository.getItems(Food::class.java, any()) } answers {
Flowable.just((content.eggs + content.eggs).sortedBy { it.key })
}
fragment.itemType = "food"

View file

@ -29,7 +29,7 @@ import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import com.habitrpg.android.habitica.modules.UserModule
import com.habitrpg.android.habitica.modules.UserRepositoryModule
@ -86,7 +86,7 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
}
setupCoil()
RxErrorHandler.init(analyticsManager)
ExceptionHandler.init(analyticsManager)
FirebaseAnalytics.getInstance(this).setUserProperty("app_testing_level", BuildConfig.TESTING_LEVEL)
@ -120,7 +120,6 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
.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

View file

@ -26,13 +26,13 @@ import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.common.habitica.models.HabitResponse
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.PurchaseValidationResult
import com.habitrpg.common.habitica.models.auth.UserAuth
import com.habitrpg.common.habitica.models.auth.UserAuthResponse
import com.habitrpg.common.habitica.models.auth.UserAuthSocial
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import com.habitrpg.common.habitica.models.HabitResponse
import com.habitrpg.shared.habitica.models.responses.Status
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
@ -49,27 +49,27 @@ import retrofit2.http.Query
@JvmSuppressWildcards
interface ApiService {
@get:GET("status")
val status: Flowable<HabitResponse<Status>>
@GET("status")
suspend fun getStatus(): HabitResponse<Status>
/* user API */
@get:GET("user/")
val user: Flowable<HabitResponse<User>>
@GET("user/")
suspend fun getUser(): HabitResponse<User>
@GET("inbox/messages")
fun getInboxMessages(@Query("conversation") uuid: String, @Query("page") page: Int): Flowable<HabitResponse<List<ChatMessage>>>
suspend fun getInboxMessages(@Query("conversation") uuid: String, @Query("page") page: Int): HabitResponse<List<ChatMessage>>
@GET("inbox/conversations")
fun getInboxConversations(): Flowable<HabitResponse<List<InboxConversation>>>
@get:GET("tasks/user")
val tasks: Flowable<HabitResponse<TaskList>>
@GET("tasks/user")
suspend fun getTasks(): HabitResponse<TaskList>
@get:GET("world-state")
val worldState: Flowable<HabitResponse<WorldState>>
@GET("world-state")
suspend fun worldState(): HabitResponse<WorldState>
@GET("content")
fun getContent(@Query("language") language: String?): Flowable<HabitResponse<ContentResult>>
suspend fun getContent(@Query("language") language: String?): HabitResponse<ContentResult>
@PUT("user/")
fun updateUser(@Body updateDictionary: Map<String, Any>): Flowable<HabitResponse<User>>
@ -130,7 +130,7 @@ interface ApiService {
fun getTask(@Path("id") id: String): Flowable<HabitResponse<Task>>
@POST("tasks/{id}/score/{direction}")
fun postTaskDirection(@Path("id") id: String, @Path("direction") direction: String): Flowable<HabitResponse<TaskDirectionData>>
suspend fun postTaskDirection(@Path("id") id: String, @Path("direction") direction: String): HabitResponse<TaskDirectionData>
@POST("tasks/bulk-score")
fun bulkScoreTasks(@Body data: List<Map<String, String>>): Flowable<HabitResponse<BulkTaskScoringData>>
@ -138,7 +138,7 @@ interface ApiService {
fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): Flowable<HabitResponse<List<String>>>
@POST("tasks/{taskId}/checklist/{itemId}/score")
fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): Flowable<HabitResponse<Task>>
suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): HabitResponse<Task>
@POST("tasks/user")
fun createTask(@Body item: Task): Flowable<HabitResponse<Task>>
@ -177,10 +177,10 @@ interface ApiService {
fun loginApple(@Body auth: Map<String, Any>): Flowable<HabitResponse<UserAuthResponse>>
@POST("user/sleep")
fun sleep(): Flowable<HabitResponse<Boolean>>
suspend fun sleep(): HabitResponse<Boolean>
@POST("user/revive")
fun revive(): Flowable<HabitResponse<User>>
suspend fun revive(): HabitResponse<User>
@POST("user/class/cast/{skill}")
fun useSkill(
@ -193,13 +193,13 @@ interface ApiService {
fun useSkill(@Path("skill") skillName: String, @Query("targetType") targetType: String): Flowable<HabitResponse<SkillResponse>>
@POST("user/change-class")
fun changeClass(): Flowable<HabitResponse<User>>
suspend fun changeClass(): HabitResponse<User>
@POST("user/change-class")
fun changeClass(@Query("class") className: String): Flowable<HabitResponse<User>>
suspend fun changeClass(@Query("class") className: String): HabitResponse<User>
@POST("user/disable-classes")
fun disableClasses(): Flowable<HabitResponse<User>>
suspend fun disableClasses(): HabitResponse<User>
@POST("user/mark-pms-read")
fun markPrivateMessagesRead(): Flowable<Void>
@ -210,25 +210,25 @@ interface ApiService {
fun listGroups(@Query("type") type: String): Flowable<HabitResponse<List<Group>>>
@GET("groups/{gid}")
fun getGroup(@Path("gid") groupId: String): Flowable<HabitResponse<Group>>
suspend fun getGroup(@Path("gid") groupId: String): HabitResponse<Group>
@POST("groups")
fun createGroup(@Body item: Group): Flowable<HabitResponse<Group>>
suspend fun createGroup(@Body item: Group): HabitResponse<Group>
@PUT("groups/{id}")
fun updateGroup(@Path("id") id: String, @Body item: Group): Flowable<HabitResponse<Group>>
suspend fun updateGroup(@Path("id") id: String, @Body item: Group): HabitResponse<Group>
@POST("groups/{groupID}/removeMember/{userID}")
fun removeMemberFromGroup(@Path("groupID") groupID: String, @Path("userID") userID: String): Flowable<HabitResponse<Void>>
suspend fun removeMemberFromGroup(@Path("groupID") groupID: String, @Path("userID") userID: String): HabitResponse<Void>
@GET("groups/{gid}/chat")
fun listGroupChat(@Path("gid") groupId: String): Flowable<HabitResponse<List<ChatMessage>>>
suspend fun listGroupChat(@Path("gid") groupId: String): HabitResponse<List<ChatMessage>>
@POST("groups/{gid}/join")
fun joinGroup(@Path("gid") groupId: String): Flowable<HabitResponse<Group>>
suspend fun joinGroup(@Path("gid") groupId: String): HabitResponse<Group>
@POST("groups/{gid}/leave")
fun leaveGroup(@Path("gid") groupId: String, @Query("keepChallenges") keepChallenges: String): Flowable<HabitResponse<Void>>
suspend fun leaveGroup(@Path("gid") groupId: String, @Query("keepChallenges") keepChallenges: String): HabitResponse<Void>
@POST("groups/{gid}/chat")
fun postGroupChat(@Path("gid") groupId: String, @Body message: Map<String, String>): Flowable<HabitResponse<PostChatMessageResult>>
@ -240,17 +240,17 @@ interface ApiService {
fun deleteInboxMessage(@Path("messageId") messageId: String): Flowable<HabitResponse<Void>>
@GET("groups/{gid}/members")
fun getGroupMembers(
suspend fun getGroupMembers(
@Path("gid") groupId: String,
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?
): Flowable<HabitResponse<List<Member>>>
): HabitResponse<List<Member>>
@GET("groups/{gid}/members")
fun getGroupMembers(
suspend fun getGroupMembers(
@Path("gid") groupId: String,
@Query("includeAllPublicFields") includeAllPublicFields: Boolean?,
@Query("lastId") lastId: String
): Flowable<HabitResponse<List<Member>>>
): HabitResponse<List<Member>>
// Like returns the full chat list
@POST("groups/{gid}/chat/{mid}/like")
@ -300,7 +300,7 @@ interface ApiService {
fun validateSubscription(@Body request: PurchaseValidationRequest): Flowable<HabitResponse<Void>>
@GET("/iap/android/subscribe/cancel")
fun cancelSubscription(): Flowable<HabitResponse<Void>>
suspend fun cancelSubscription(): HabitResponse<Void>
@POST("/iap/android/norenew-subscribe")
fun validateNoRenewSubscription(@Body request: PurchaseValidationRequest): Flowable<HabitResponse<Void>>
@ -310,16 +310,16 @@ interface ApiService {
// Members URL
@GET("members/{mid}")
fun getMember(@Path("mid") memberId: String): Flowable<HabitResponse<Member>>
suspend fun getMember(@Path("mid") memberId: String): HabitResponse<Member>
@GET("members/username/{username}")
fun getMemberWithUsername(@Path("username") username: String): Flowable<HabitResponse<Member>>
suspend fun getMemberWithUsername(@Path("username") username: String): HabitResponse<Member>
@GET("members/{mid}/achievements")
fun getMemberAchievements(@Path("mid") memberId: String, @Query("lang") language: String?): Flowable<HabitResponse<List<Achievement>>>
@POST("members/send-private-message")
fun postPrivateMessage(@Body messageDetails: Map<String, String>): Flowable<HabitResponse<PostChatMessageResult>>
suspend fun postPrivateMessage(@Body messageDetails: Map<String, String>): HabitResponse<PostChatMessageResult>
@GET("members/find/{username}")
fun findUsernames(
@ -440,7 +440,7 @@ interface ApiService {
fun blockMember(@Path("userID") userID: String): Flowable<HabitResponse<List<String>>>
@POST("user/reroll")
fun reroll(): Flowable<HabitResponse<User>>
suspend fun reroll(): HabitResponse<User>
// Team Plans
@ -448,5 +448,5 @@ interface ApiService {
fun getTeamPlans(): Flowable<HabitResponse<List<TeamPlan>>>
@GET("tasks/group/{groupID}")
fun getTeamPlanTasks(@Path("groupID") groupId: String): Flowable<HabitResponse<TaskList>>
suspend fun getTeamPlanTasks(@Path("groupID") groupId: String): HabitResponse<TaskList>
}

View file

@ -6,6 +6,8 @@ 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;
@ -101,6 +103,8 @@ public class GSonFactoryCreator {
}.getType();
Type achievementsListType = new TypeToken<List<Achievement>>() {
}.getType();
Type assignedDetailsListType = new TypeToken<RealmList<GroupAssignedDetails>>() {
}.getType();
return new GsonBuilder()
.registerTypeAdapter(taskTagClassListType, new TaskTagDeserializer())
@ -129,6 +133,7 @@ public class GSonFactoryCreator {
.registerTypeAdapter(ownedPetListType, new OwnedPetListDeserializer())
.registerTypeAdapter(ownedMountListType, new OwnedMountListDeserializer())
.registerTypeAdapter(achievementsListType, new AchievementListDeserializer())
.registerTypeAdapter(assignedDetailsListType, new AssignedDetailsDeserializer())
.registerTypeAdapter(Quest.class, new QuestDeserializer())
.registerTypeAdapter(Member.class, new MemberSerialization())
.registerTypeAdapter(WorldState.class, new WorldStateSerialization())

View file

@ -38,6 +38,8 @@ import com.habitrpg.android.habitica.ui.activities.SetupActivity;
import com.habitrpg.android.habitica.ui.activities.SkillMemberActivity;
import com.habitrpg.android.habitica.ui.activities.SkillTasksActivity;
import com.habitrpg.android.habitica.ui.activities.TaskFormActivity;
import com.habitrpg.android.habitica.ui.activities.TaskSummaryActivity;
import com.habitrpg.android.habitica.ui.activities.TaskSummaryViewModel;
import com.habitrpg.android.habitica.ui.activities.VerifyUsernameActivity;
import com.habitrpg.android.habitica.ui.adapter.social.challenges.ChallengeTasksRecyclerViewAdapter;
import com.habitrpg.android.habitica.ui.adapter.tasks.DailiesRecyclerViewHolder;
@ -52,10 +54,8 @@ import com.habitrpg.android.habitica.ui.fragments.PromoInfoFragment;
import com.habitrpg.android.habitica.ui.fragments.PromoWebFragment;
import com.habitrpg.android.habitica.ui.fragments.StatsFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarCustomizationFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarEquipmentFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.customization.AvatarOverviewFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.equipment.EquipmentDetailFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.equipment.EquipmentOverviewFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.items.ItemDialogFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.items.ItemRecyclerFragment;
import com.habitrpg.android.habitica.ui.fragments.inventory.items.ItemsFragment;
@ -165,8 +165,6 @@ public interface UserComponent {
void inject(EquipmentDetailFragment equipmentDetailFragment);
void inject(EquipmentOverviewFragment equipmentOverviewFragment);
void inject(ItemRecyclerFragment itemRecyclerFragment);
void inject(ItemsFragment itemsFragment);
@ -333,8 +331,6 @@ public interface UserComponent {
void inject(BugFixFragment bugFixFragment);
void inject(AvatarEquipmentFragment avatarEquipmentFragment);
void inject(FAQDetailFragment faqDetailFragment);
void inject(AdventureGuideActivity adventureGuideFragment);
@ -370,4 +366,8 @@ public interface UserComponent {
void inject(@NotNull DeathActivity deathActivity);
void inject(@NotNull DeviceCommunicationService deviceCommunicationService);
void inject(@NotNull TaskSummaryActivity taskSummaryActivity);
void inject(@NotNull TaskSummaryViewModel taskSummaryViewModel);
}

View file

@ -27,12 +27,12 @@ import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.common.habitica.api.HostConfig
import com.habitrpg.common.habitica.models.HabitResponse
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.PurchaseValidationResult
import com.habitrpg.common.habitica.models.auth.UserAuthResponse
import com.habitrpg.shared.habitica.models.responses.ErrorResponse
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import com.habitrpg.common.habitica.models.HabitResponse
import com.habitrpg.shared.habitica.models.responses.Status
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
@ -44,23 +44,19 @@ interface ApiClient {
val hostConfig: HostConfig
val status: Flowable<Status>
val content: Flowable<ContentResult>
suspend fun getStatus(): Status?
/* user API */
val user: Flowable<User>
val tasks: Flowable<TaskList>
suspend fun getTasks(): TaskList?
/* challenges api */
fun getUserChallenges(page: Int, memberOnly: Boolean): Flowable<List<Challenge>>
val worldState: Flowable<WorldState>
suspend fun getWorldState(): WorldState?
fun setLanguageCode(languageCode: String)
fun getContent(language: String): Flowable<ContentResult>
suspend fun getContent(language: String? = null): ContentResult?
fun updateUser(updateDictionary: Map<String, Any>): Flowable<User>
@ -83,7 +79,7 @@ interface ApiClient {
fun purchaseSpecialSpell(key: String): Flowable<Void>
fun validateSubscription(request: PurchaseValidationRequest): Flowable<Any>
fun validateNoRenewSubscription(request: PurchaseValidationRequest): Flowable<Any>
fun cancelSubscription(): Flowable<Void>
suspend fun cancelSubscription(): Void?
fun sellItem(itemType: String, itemKey: String): Flowable<User>
@ -97,12 +93,12 @@ interface ApiClient {
fun getTask(id: String): Flowable<Task>
fun postTaskDirection(id: String, direction: String): Flowable<TaskDirectionData>
suspend fun postTaskDirection(id: String, direction: String): TaskDirectionData?
fun bulkScoreTasks(data: List<Map<String, String>>): Flowable<BulkTaskScoringData>
fun postTaskNewPosition(id: String, position: Int): Flowable<List<String>>
fun scoreChecklistItem(taskId: String, itemId: String): Flowable<Task>
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
fun createTask(item: Task): Flowable<Task>
@ -127,19 +123,16 @@ interface ApiClient {
fun loginApple(authToken: String): Flowable<UserAuthResponse>
fun sleep(): Flowable<Boolean>
fun revive(): Flowable<User>
suspend fun sleep(): Boolean?
suspend fun revive(): User?
fun useSkill(skillName: String, targetType: String, targetId: String): Flowable<SkillResponse>
fun useSkill(skillName: String, targetType: String): Flowable<SkillResponse>
fun changeClass(): Flowable<User>
suspend fun changeClass(className: String?): User?
fun changeClass(className: String): Flowable<User>
fun disableClasses(): Flowable<User>
suspend fun disableClasses(): User?
fun markPrivateMessagesRead(): Flowable<Void>
@ -147,26 +140,26 @@ interface ApiClient {
fun listGroups(type: String): Flowable<List<Group>>
fun getGroup(groupId: String): Flowable<Group>
suspend fun getGroup(groupId: String): Group?
fun createGroup(group: Group): Flowable<Group>
fun updateGroup(id: String, item: Group): Flowable<Group>
fun removeMemberFromGroup(groupID: String, userID: String): Flowable<Void>
suspend fun createGroup(group: Group): Group?
suspend fun updateGroup(id: String, item: Group): Group?
suspend fun removeMemberFromGroup(groupID: String, userID: String): Void?
fun listGroupChat(groupId: String): Flowable<List<ChatMessage>>
suspend fun listGroupChat(groupId: String): List<ChatMessage>?
fun joinGroup(groupId: String): Flowable<Group>
suspend fun joinGroup(groupId: String): Group?
fun leaveGroup(groupId: String, keepChallenges: String): Flowable<Void>
suspend fun leaveGroup(groupId: String, keepChallenges: String): Void?
fun postGroupChat(groupId: String, message: Map<String, String>): Flowable<PostChatMessageResult>
fun deleteMessage(groupId: String, messageId: String): Flowable<Void>
fun deleteInboxMessage(id: String): Flowable<Void>
fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): Flowable<List<Member>>
suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): List<Member>?
fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): Flowable<List<Member>>
suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): List<Member>?
// Like returns the full chat list
fun likeMessage(groupId: String, mid: String): Flowable<ChatMessage>
@ -199,12 +192,12 @@ interface ApiClient {
fun changeCustomDayStart(updateObject: Map<String, Any>): Flowable<User>
// Members URL
fun getMember(memberId: String): Flowable<Member>
fun getMemberWithUsername(username: String): Flowable<Member>
suspend fun getMember(memberId: String): Member?
suspend fun getMemberWithUsername(username: String): Member?
fun getMemberAchievements(memberId: String): Flowable<List<Achievement>>
fun postPrivateMessage(messageDetails: Map<String, String>): Flowable<PostChatMessageResult>
suspend fun postPrivateMessage(messageDetails: Map<String, String>): PostChatMessageResult?
fun retrieveShopIventory(identifier: String): Flowable<Shop>
@ -243,8 +236,8 @@ interface ApiClient {
fun hasAuthenticationKeys(): Boolean
fun retrieveUser(withTasks: Boolean): Flowable<User>
fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>>
suspend fun retrieveUser(withTasks: Boolean = false): User?
suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>?
fun retrieveInboxConversations(): Flowable<List<InboxConversation>>
fun <T : Any> configureApiCallObserver(): FlowableTransformer<HabitResponse<T>, T>
@ -253,7 +246,7 @@ interface ApiClient {
fun runCron(): Flowable<Void>
fun reroll(): Flowable<User>
suspend fun reroll(): User?
fun resetAccount(): Flowable<Void>
fun deleteAccount(password: String): Flowable<Void>
@ -282,5 +275,5 @@ interface ApiClient {
fun unlinkAllTasks(challengeID: String?, keepOption: String): Flowable<Void>
fun blockMember(userID: String): Flowable<List<String>>
fun getTeamPlans(): Flowable<List<TeamPlan>>
fun getTeamPlanTasks(teamID: String): Flowable<TaskList>
suspend fun getTeamPlanTasks(teamID: String): TaskList?
}

View file

@ -4,9 +4,9 @@ import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState
import io.reactivex.rxjava3.core.Flowable
interface ContentRepository : BaseRepository {
fun retrieveContent(forced: Boolean = false): Flowable<ContentResult>
interface ContentRepository: BaseRepository {
suspend fun retrieveContent(forced: Boolean = false): ContentResult?
fun retrieveWorldState(): Flowable<WorldState>
suspend fun retrieveWorldState(): WorldState?
fun getWorldState(): Flowable<WorldState>
}

View file

@ -35,7 +35,7 @@ interface InventoryRepository : BaseRepository {
fun getPets(): Flow<List<Pet>>
fun getOwnedPets(): Flow<List<OwnedPet>>
fun getQuestContent(key: String): Flowable<QuestContent>
fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flowable<out List<Equipment>>
@ -86,7 +86,7 @@ interface InventoryRepository : BaseRepository {
fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Flowable<Void>
fun togglePinnedItem(item: ShopItem): Flowable<List<ShopItem>>
fun getItemsFlowable(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>>
fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>>
fun getItemsFlowable(itemClass: Class<out Item>): Flowable<out List<Item>>
fun getItems(itemClass: Class<out Item>): Flow<List<Item>>
fun getLatestMysteryItem(): Flowable<Equipment>

View file

@ -11,14 +11,14 @@ import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface SocialRepository : BaseRepository {
fun getPublicGuilds(): Flowable<out List<Group>>
fun getUserGroups(type: String?): Flowable<out List<Group>>
fun retrieveGroupChat(groupId: String): Single<List<ChatMessage>>
fun getUserGroups(type: String?): Flow<List<Group>>
suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>?
fun getGroupChat(groupId: String): Flowable<out List<ChatMessage>>
fun markMessagesSeen(seenGroupId: String)
@ -40,52 +40,52 @@ interface SocialRepository : BaseRepository {
fun postGroupChat(groupId: String, message: String): Flowable<PostChatMessageResult>
fun retrieveGroup(id: String): Flowable<Group>
suspend fun retrieveGroup(id: String): Group?
fun getGroup(id: String?): Flow<Group?>
fun getGroupFlowable(id: String?): Flowable<Group>
fun leaveGroup(id: String?, keepChallenges: Boolean): Flowable<Group>
suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group?
fun joinGroup(id: String?): Flowable<Group>
suspend fun joinGroup(id: String?): Group?
fun createGroup(
suspend fun createGroup(
name: String?,
description: String?,
leader: String?,
type: String?,
privacy: String?,
leaderCreateChallenge: Boolean?
): Flowable<Group>
): Group?
fun updateGroup(
suspend fun updateGroup(
group: Group?,
name: String?,
description: String?,
leader: String?,
leaderCreateChallenge: Boolean?
): Flowable<Group>
): Group?
fun retrieveGroups(type: String): Flowable<List<Group>>
fun getGroups(type: String): Flowable<out List<Group>>
fun getInboxMessages(replyToUserID: String?): Flowable<out List<ChatMessage>>
fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>>
fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>>
suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>?
fun retrieveInboxConversations(): Flowable<List<InboxConversation>>
fun getInboxConversations(): Flowable<out List<InboxConversation>>
fun postPrivateMessage(
fun getInboxConversations(): Flow<RealmResults<InboxConversation>>
suspend fun postPrivateMessage(
recipientId: String,
messageObject: HashMap<String, String>
): Flowable<List<ChatMessage>>
): List<ChatMessage>?
fun postPrivateMessage(recipientId: String, message: String): Flowable<List<ChatMessage>>
suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>?
fun getGroupMembers(id: String): Flow<List<Member>>
fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable<List<Member>>
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>?
fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<List<Void>>
fun getMember(userId: String?): Flowable<Member>
fun getMemberWithUsername(username: String?): Flowable<Member>
suspend fun retrieveMember(userId: String?): Member?
suspend fun retrieveMemberWithUsername(username: String?): Member?
fun findUsernames(
username: String,
@ -97,8 +97,8 @@ interface SocialRepository : BaseRepository {
fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>)
fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group>
fun removeMemberFromGroup(groupID: String, userID: String): Flowable<List<Member>>
suspend fun transferGroupOwnership(groupID: String, userID: String): Group?
suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>?
fun acceptQuest(user: User?, partyId: String = "party"): Flowable<Void>
fun rejectQuest(user: User?, partyId: String = "party"): Flowable<Void>
@ -117,7 +117,7 @@ interface SocialRepository : BaseRepository {
fun transferGems(giftedID: String, amount: Int): Flowable<Void>
fun getGroupMembership(id: String): Flowable<GroupMembership>
fun getGroupMembership(id: String): Flow<GroupMembership?>
fun getGroupMemberships(): Flowable<out List<GroupMembership>>
fun blockMember(userID: String): Flowable<List<String>>
}

View file

@ -19,27 +19,27 @@ interface TaskRepository : BaseRepository {
fun getTasksFlowable(taskType: TaskType, userID: String? = null, includedGroupIDs: Array<String>): Flowable<out List<Task>>
fun saveTasks(userId: String, order: TasksOrder, tasks: TaskList)
fun retrieveTasks(userId: String, tasksOrder: TasksOrder): Flowable<TaskList>
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList?
fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): Flowable<TaskList>
fun taskChecked(
suspend fun taskChecked(
user: User?,
task: Task,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
): Flowable<TaskScoringResult>
fun taskChecked(
): TaskScoringResult?
suspend fun taskChecked(
user: User?,
taskId: String,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
): Maybe<TaskScoringResult>
fun scoreChecklistItem(taskId: String, itemId: String): Flowable<Task>
): TaskScoringResult?
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
fun getTask(taskId: String): Flowable<Task>
fun getTaskCopy(taskId: String): Flowable<Task>
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task>
fun createTask(task: Task, force: Boolean = false): Flowable<Task>
fun updateTask(task: Task, force: Boolean = false): Maybe<Task>?
fun deleteTask(taskId: String): Flowable<Void>
@ -55,7 +55,7 @@ interface TaskRepository : BaseRepository {
fun updateTaskPosition(taskType: TaskType, taskID: String, newPosition: Int): Maybe<List<String>>
fun getUnmanagedTask(taskid: String): Flowable<Task>
fun getUnmanagedTask(taskid: String): Flow<Task>
fun updateTaskInBackground(task: Task)

View file

@ -26,18 +26,13 @@ interface UserRepository : BaseRepository {
fun updateUser(updateData: Map<String, Any>): Flowable<User>
fun updateUser(key: String, value: Any): Flowable<User>
fun retrieveUser(withTasks: Boolean): Flowable<User>
fun retrieveUser(
withTasks: Boolean = false,
forced: Boolean = false,
overrideExisting: Boolean = false
): Flowable<User>
suspend fun retrieveUser(withTasks: Boolean = false, forced: Boolean = false, overrideExisting: Boolean = false): User?
fun revive(): Flowable<User>
suspend fun revive(): User?
fun resetTutorial(): Maybe<User>
fun sleep(user: User): Flowable<User>
suspend fun sleep(user: User): User?
fun getSkills(user: User): Flowable<out List<Skill>>
@ -46,17 +41,14 @@ interface UserRepository : BaseRepository {
fun useSkill(key: String, target: String?, taskId: String): Flowable<SkillResponse>
fun useSkill(key: String, target: String?): Flowable<SkillResponse>
fun changeClass(): Flowable<User>
fun disableClasses(): Flowable<User>
fun changeClass(selectedClass: String): Flowable<User>
suspend fun disableClasses(): User?
suspend fun changeClass(selectedClass: String? = null): User?
fun unlockPath(path: String, price: Int): Flowable<UnlockResponse>
fun unlockPath(customization: Customization): Flowable<UnlockResponse>
fun runCron(tasks: MutableList<Task>)
fun runCron()
suspend fun runCron(tasks: MutableList<Task>)
suspend fun runCron()
fun readNotification(id: String): Flowable<List<Any>>
fun readNotifications(notificationIds: Map<String, List<String>>): Flowable<List<Any>>
@ -66,7 +58,7 @@ interface UserRepository : BaseRepository {
fun updateLanguage(languageCode: String): Flowable<User>
fun resetAccount(): Flowable<User>
suspend fun resetAccount(): User?
fun deleteAccount(password: String): Flowable<Void>
fun sendPasswordResetEmail(email: String): Flowable<Void>
@ -86,9 +78,9 @@ interface UserRepository : BaseRepository {
fun getUserQuestStatus(): Flowable<UserQuestStatus>
fun reroll(): Flowable<User>
suspend fun reroll(): User?
fun retrieveTeamPlans(): Flowable<List<TeamPlan>>
fun getTeamPlans(): Flow<List<TeamPlan>>
fun retrieveTeamPlan(teamID: String): Flowable<Group>
suspend fun retrieveTeamPlan(teamID: String): Group?
fun getTeamPlan(teamID: String): Flowable<Group>
}

View file

@ -9,7 +9,6 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.api.ApiService
import com.habitrpg.android.habitica.api.GSonFactoryCreator
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.extensions.filterMap
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.ContentResult
@ -40,6 +39,7 @@ import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.common.habitica.api.HostConfig
import com.habitrpg.common.habitica.api.Server
import com.habitrpg.common.habitica.models.HabitResponse
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.PurchaseValidationResult
import com.habitrpg.common.habitica.models.auth.UserAuth
@ -48,7 +48,6 @@ import com.habitrpg.common.habitica.models.auth.UserAuthSocial
import com.habitrpg.common.habitica.models.auth.UserAuthSocialTokens
import com.habitrpg.shared.habitica.models.responses.ErrorResponse
import com.habitrpg.shared.habitica.models.responses.FeedResponse
import com.habitrpg.common.habitica.models.HabitResponse
import com.habitrpg.shared.habitica.models.responses.Status
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
@ -90,19 +89,31 @@ class ApiClientImpl(
private val apiCallTransformer = FlowableTransformer<HabitResponse<Any>, Any> { observable ->
observable
.filterMap { habitResponse ->
habitResponse.notifications?.let {
notificationsManager.setNotifications(it)
}
if (hadError) {
hideConnectionProblemDialog()
}
habitResponse.data
.filter { it.data != null }
.map { habitResponse ->
processResponse(habitResponse)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(this)
}
private fun <T> processResponse(habitResponse: HabitResponse<T>): T? {
habitResponse.notifications?.let {
notificationsManager.setNotifications(it)
}
return habitResponse.data
}
suspend fun <T> handleSuspendCall(apiCall: suspend () -> HabitResponse<T>): T? {
try {
return processResponse(apiCall())
} catch (throwable: Throwable) {
accept(throwable)
}
return null
}
private var languageCode: String? = null
private var lastAPICallURL: String? = null
private var hadError = false
@ -268,25 +279,15 @@ class ApiClientImpl(
}
}
override fun retrieveUser(withTasks: Boolean): Flowable<User> {
var userObservable = this.user
if (withTasks) {
val tasksObservable = this.tasks
userObservable = Flowable.zip(
userObservable, tasksObservable
) { habitRPGUser, tasks ->
habitRPGUser.tasks = tasks
habitRPGUser
}
}
return userObservable
override suspend fun retrieveUser(withTasks: Boolean): User? {
val user = handleSuspendCall { apiService.getUser() }
val tasks = getTasks()
user?.tasks = tasks
return user
}
override fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>> {
return apiService.getInboxMessages(uuid, page).compose(configureApiCallObserver())
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? {
return handleSuspendCall { apiService.getInboxMessages(uuid, page) }
}
override fun retrieveInboxConversations(): Flowable<List<InboxConversation>> {
@ -345,16 +346,12 @@ class ApiClientImpl(
this.languageCode = languageCode
}
override val status: Flowable<Status>
get() = apiService.status.compose(configureApiCallObserver())
override suspend fun getStatus(): Status? = handleSuspendCall { apiService.getStatus() }
override fun getContent(language: String): Flowable<ContentResult> {
return apiService.getContent(language).compose(configureApiCallObserver())
override suspend fun getContent(language: String?): ContentResult? {
return handleSuspendCall { apiService.getContent(language) }
}
override val user: Flowable<User>
get() = apiService.user.compose(configureApiCallObserver())
override fun updateUser(updateDictionary: Map<String, Any>): Flowable<User> {
return apiService.updateUser(updateDictionary).compose(configureApiCallObserver())
}
@ -399,8 +396,8 @@ class ApiClientImpl(
return apiService.validateNoRenewSubscription(request).compose(configureApiCallObserver())
}
override fun cancelSubscription(): Flowable<Void> {
return apiService.cancelSubscription().compose(configureApiCallObserver())
override suspend fun cancelSubscription(): Void? {
return processResponse(apiService.cancelSubscription())
}
override fun purchaseHourglassItem(type: String, itemKey: String): Flowable<Void> {
@ -436,8 +433,7 @@ class ApiClientImpl(
return apiService.hatchPet(eggKey, hatchingPotionKey).compose(configureApiCallObserver())
}
override val tasks: Flowable<TaskList>
get() = apiService.tasks.compose(configureApiCallObserver())
override suspend fun getTasks(): TaskList? = handleSuspendCall { apiService.getTasks() }
override fun getTasks(type: String): Flowable<TaskList> {
return apiService.getTasks(type).compose(configureApiCallObserver())
@ -455,8 +451,8 @@ class ApiClientImpl(
return apiService.getTask(id).compose(configureApiCallObserver())
}
override fun postTaskDirection(id: String, direction: String): Flowable<TaskDirectionData> {
return apiService.postTaskDirection(id, direction).compose(configureApiCallObserver())
override suspend fun postTaskDirection(id: String, direction: String): TaskDirectionData? {
return handleSuspendCall { apiService.postTaskDirection(id, direction) }
}
override fun bulkScoreTasks(data: List<Map<String, String>>): Flowable<BulkTaskScoringData> {
@ -467,8 +463,8 @@ class ApiClientImpl(
return apiService.postTaskNewPosition(id, position).compose(configureApiCallObserver())
}
override fun scoreChecklistItem(taskId: String, itemId: String): Flowable<Task> {
return apiService.scoreChecklistItem(taskId, itemId).compose(configureApiCallObserver())
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? {
return handleSuspendCall { apiService.scoreChecklistItem(taskId, itemId) }
}
override fun createTask(item: Task): Flowable<Task> {
@ -499,13 +495,9 @@ class ApiClientImpl(
return apiService.deleteTag(id).compose(configureApiCallObserver())
}
override fun sleep(): Flowable<Boolean> {
return apiService.sleep().compose(configureApiCallObserver())
}
override suspend fun sleep(): Boolean? = handleSuspendCall { apiService.sleep() }
override fun revive(): Flowable<User> {
return apiService.revive().compose(configureApiCallObserver())
}
override suspend fun revive(): User? = handleSuspendCall { apiService.revive() }
override fun useSkill(skillName: String, targetType: String, targetId: String): Flowable<SkillResponse> {
return apiService.useSkill(skillName, targetType, targetId).compose(configureApiCallObserver())
@ -515,17 +507,17 @@ class ApiClientImpl(
return apiService.useSkill(skillName, targetType).compose(configureApiCallObserver())
}
override fun changeClass(): Flowable<User> {
return apiService.changeClass().compose(configureApiCallObserver())
override suspend fun changeClass(className: String?): User? {
return handleSuspendCall {
if (className != null) {
apiService.changeClass(className)
} else {
apiService.changeClass()
}
}
}
override fun changeClass(className: String): Flowable<User> {
return apiService.changeClass(className).compose(configureApiCallObserver())
}
override fun disableClasses(): Flowable<User> {
return apiService.disableClasses().compose(configureApiCallObserver())
}
override suspend fun disableClasses(): User? = handleSuspendCall { apiService.disableClasses() }
override fun markPrivateMessagesRead(): Flowable<Void> {
// This is necessary, because the API call returns weird data.
@ -539,32 +531,32 @@ class ApiClientImpl(
return apiService.listGroups(type).compose(configureApiCallObserver())
}
override fun getGroup(groupId: String): Flowable<Group> {
return apiService.getGroup(groupId).compose(configureApiCallObserver())
override suspend fun getGroup(groupId: String): Group? {
return processResponse(apiService.getGroup(groupId))
}
override fun createGroup(group: Group): Flowable<Group> {
return apiService.createGroup(group).compose(configureApiCallObserver())
override suspend fun createGroup(group: Group): Group? {
return processResponse(apiService.createGroup(group))
}
override fun updateGroup(id: String, item: Group): Flowable<Group> {
return apiService.updateGroup(id, item).compose(configureApiCallObserver())
override suspend fun updateGroup(id: String, item: Group): Group? {
return processResponse(apiService.updateGroup(id, item))
}
override fun removeMemberFromGroup(groupID: String, userID: String): Flowable<Void> {
return apiService.removeMemberFromGroup(groupID, userID).compose(configureApiCallObserver())
override suspend fun removeMemberFromGroup(groupID: String, userID: String): Void? {
return processResponse(apiService.removeMemberFromGroup(groupID, userID))
}
override fun listGroupChat(groupId: String): Flowable<List<ChatMessage>> {
return apiService.listGroupChat(groupId).compose(configureApiCallObserver())
override suspend fun listGroupChat(groupId: String): List<ChatMessage>? {
return processResponse(apiService.listGroupChat(groupId))
}
override fun joinGroup(groupId: String): Flowable<Group> {
return apiService.joinGroup(groupId).compose(configureApiCallObserver())
override suspend fun joinGroup(groupId: String): Group? {
return processResponse(apiService.joinGroup(groupId))
}
override fun leaveGroup(groupId: String, keepChallenges: String): Flowable<Void> {
return apiService.leaveGroup(groupId, keepChallenges).compose(configureApiCallObserver())
override suspend fun leaveGroup(groupId: String, keepChallenges: String): Void? {
return processResponse(apiService.leaveGroup(groupId, keepChallenges))
}
override fun postGroupChat(groupId: String, message: Map<String, String>): Flowable<PostChatMessageResult> {
@ -578,12 +570,12 @@ class ApiClientImpl(
return apiService.deleteInboxMessage(id).compose(configureApiCallObserver())
}
override fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): Flowable<List<Member>> {
return apiService.getGroupMembers(groupId, includeAllPublicFields).compose(configureApiCallObserver())
override suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): List<Member>? {
return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields))
}
override fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): Flowable<List<Member>> {
return apiService.getGroupMembers(groupId, includeAllPublicFields, lastId).compose(configureApiCallObserver())
override suspend fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): List<Member>? {
return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields, lastId))
}
override fun likeMessage(groupId: String, mid: String): Flowable<ChatMessage> {
@ -646,13 +638,8 @@ class ApiClientImpl(
return apiService.changeCustomDayStart(updateObject).compose(configureApiCallObserver())
}
override fun getMember(memberId: String): Flowable<Member> {
return apiService.getMember(memberId).compose(configureApiCallObserver())
}
override fun getMemberWithUsername(username: String): Flowable<Member> {
return apiService.getMemberWithUsername(username).compose(configureApiCallObserver())
}
override suspend fun getMember(memberId: String) = processResponse(apiService.getMember(memberId))
override suspend fun getMemberWithUsername(username: String) = processResponse(apiService.getMemberWithUsername(username))
override fun getMemberAchievements(memberId: String): Flowable<List<Achievement>> {
return apiService.getMemberAchievements(memberId, languageCode).compose(configureApiCallObserver())
@ -662,8 +649,8 @@ class ApiClientImpl(
return apiService.findUsernames(username, context, id).compose(configureApiCallObserver())
}
override fun postPrivateMessage(messageDetails: Map<String, String>): Flowable<PostChatMessageResult> {
return apiService.postPrivateMessage(messageDetails).compose(configureApiCallObserver())
override suspend fun postPrivateMessage(messageDetails: Map<String, String>): PostChatMessageResult? {
return handleSuspendCall { apiService.postPrivateMessage(messageDetails) }
}
override fun retrieveShopIventory(identifier: String): Flowable<Shop> {
@ -738,9 +725,6 @@ class ApiClientImpl(
return apiService.seeNotifications(notificationIds).compose(configureApiCallObserver())
}
override val content: Flowable<ContentResult>
get() = apiService.getContent(languageCode).compose(configureApiCallObserver())
override fun openMysteryItem(): Flowable<Equipment> {
return apiService.openMysteryItem().compose(configureApiCallObserver())
}
@ -749,9 +733,7 @@ class ApiClientImpl(
return apiService.runCron().compose(configureApiCallObserver())
}
override fun reroll(): Flowable<User> {
return apiService.reroll().compose(configureApiCallObserver())
}
override suspend fun reroll(): User? = handleSuspendCall { apiService.reroll() }
override fun resetAccount(): Flowable<Void> {
return apiService.resetAccount().compose(configureApiCallObserver())
@ -825,8 +807,8 @@ class ApiClientImpl(
return apiService.getTeamPlans().compose(configureApiCallObserver())
}
override fun getTeamPlanTasks(teamID: String): Flowable<TaskList> {
return apiService.getTeamPlanTasks(teamID).compose(configureApiCallObserver())
override suspend fun getTeamPlanTasks(teamID: String): TaskList? {
return processResponse(apiService.getTeamPlanTasks(teamID))
}
override fun bulkAllocatePoints(
@ -849,8 +831,7 @@ class ApiClientImpl(
return apiService.retrieveMarketGear(languageCode).compose(configureApiCallObserver())
}
override val worldState: Flowable<WorldState>
get() = apiService.worldState.compose(configureApiCallObserver())
override suspend fun getWorldState(): WorldState? = handleSuspendCall { apiService.worldState() }
companion object {
fun createGsonFactory(): GsonConverterFactory {

View file

@ -9,6 +9,7 @@ import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.SpecialItem
import io.reactivex.rxjava3.core.Flowable
import io.realm.RealmList
import java.util.Date
class ContentRepositoryImpl<T : ContentLocalRepository>(
@ -22,34 +23,33 @@ class ContentRepositoryImpl<T : ContentLocalRepository>(
private var lastContentSync = 0L
private var lastWorldStateSync = 0L
override fun retrieveContent(forced: Boolean): Flowable<ContentResult> {
override suspend fun retrieveContent(forced: Boolean): ContentResult? {
val now = Date().time
return if (forced || now - this.lastContentSync > 300000) {
if (forced || now - this.lastContentSync > 300000) {
val content = apiClient.getContent() ?: return null
lastContentSync = now
apiClient.content.doOnNext {
it.special.add(mysteryItem)
localRepository.saveContent(it)
}
} else {
Flowable.just(ContentResult())
content.special = RealmList()
content.special.add(mysteryItem)
localRepository.saveContent(content)
return content
}
return null
}
override fun retrieveWorldState(): Flowable<WorldState> {
override suspend fun retrieveWorldState(): WorldState? {
val now = Date().time
return if (now - this.lastWorldStateSync > 3600000) {
if (now - this.lastWorldStateSync > 3600000) {
val state = apiClient.getWorldState() ?: return null
lastWorldStateSync = now
apiClient.worldState.doOnNext {
localRepository.saveWorldState(it)
for (event in it.events) {
if (event.aprilFools != null && event.isCurrentlyActive) {
AprilFoolsHandler.handle(event.aprilFools, event.end)
}
localRepository.save(state)
for (event in state.events) {
if (event.aprilFools != null && event.isCurrentlyActive) {
AprilFoolsHandler.handle(event.aprilFools, event.end)
}
}
} else {
Flowable.just(WorldState())
return state
}
return null
}
override fun getWorldState(): Flowable<WorldState> {

View file

@ -31,13 +31,9 @@ class InventoryRepositoryImpl(
userID: String,
var appConfigManager: AppConfigManager
) : BaseRepositoryImpl<InventoryLocalRepository>(localRepository, apiClient, userID), InventoryRepository {
override fun getQuestContent(keys: List<String>): Flow<List<QuestContent>> {
return localRepository.getQuestContent(keys)
}
override fun getQuestContent(keys: List<String>) = localRepository.getQuestContent(keys)
override fun getQuestContent(key: String): Flowable<QuestContent> {
return localRepository.getQuestContent(key)
}
override fun getQuestContent(key: String) = localRepository.getQuestContent(key)
override fun getEquipment(searchedKeys: List<String>): Flowable<out List<Equipment>> {
return localRepository.getEquipment(searchedKeys)
@ -75,7 +71,7 @@ class InventoryRepositoryImpl(
return localRepository.getOwnedItems(userID, includeZero)
}
override fun getItemsFlowable(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.getItemsFlowable(itemClass, keys)
}

View file

@ -4,7 +4,7 @@ import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.local.SocialLocalRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.members.Member
@ -16,12 +16,10 @@ import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import retrofit2.HttpException
import kotlinx.coroutines.flow.firstOrNull
import java.util.UUID
class SocialRepositoryImpl(
@ -29,45 +27,31 @@ class SocialRepositoryImpl(
apiClient: ApiClient,
userID: String
) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, userID), SocialRepository {
override fun transferGroupOwnership(groupID: String, userID: String): Flowable<Group> {
return localRepository.getGroupFlowable(groupID)
.map {
val group = localRepository.getUnmanagedCopy(it)
group.leaderID = userID
group
}
.flatMap {
apiClient.updateGroup(it.id, it)
}
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 fun removeMemberFromGroup(groupID: String, userID: String): Flowable<List<Member>> {
return apiClient.removeMemberFromGroup(groupID, userID)
.flatMap {
retrieveGroupMembers(groupID, true)
}
override suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>? {
apiClient.removeMemberFromGroup(groupID, userID)
return retrievePartyMembers(groupID, true)
}
override fun blockMember(userID: String): Flowable<List<String>> {
return apiClient.blockMember(userID)
}
override fun getGroupMembership(id: String): Flowable<GroupMembership> {
return localRepository.getGroupMembership(userID, id)
}
override fun getGroupMembership(id: String) = localRepository.getGroupMembership(userID, id)
override fun getGroupMemberships(): Flowable<out List<GroupMembership>> {
return localRepository.getGroupMemberships(userID)
}
override fun retrieveGroupChat(groupId: String): Single<List<ChatMessage>> {
return apiClient.listGroupChat(groupId)
.flatMap { Flowable.fromIterable(it) }
.map { chatMessage ->
chatMessage.groupId = groupId
chatMessage
}
.toList()
override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? {
val messages = apiClient.listGroupChat(groupId)
messages?.forEach { it.groupId = groupId }
return messages
}
override fun getGroupChat(groupId: String): Flowable<out List<ChatMessage>> {
@ -75,7 +59,7 @@ class SocialRepositoryImpl(
}
override fun markMessagesSeen(seenGroupId: String) {
apiClient.seenMessages(seenGroupId).subscribe({ }, RxErrorHandler.handleEmptyError())
apiClient.seenMessages(seenGroupId).subscribe({ }, ExceptionHandler.rx())
}
override fun flagMessage(chatMessageID: String, additionalInfo: String, groupID: String?): Flowable<Void> {
@ -131,76 +115,70 @@ class SocialRepositoryImpl(
return postGroupChat(groupId, messageObject)
}
override fun retrieveGroup(id: String): Flowable<Group> {
return Flowable.zip(
apiClient.getGroup(id).doOnNext { localRepository.saveGroup(it) },
retrieveGroupChat(id)
.toFlowable()
) { group, _ ->
group
}.doOnError {
if (it is HttpException && it.code() == 404) {
MainScope().launch {
val group = localRepository.getGroup(id).first()
if (group != null) {
localRepository.delete(group)
}
}
}
}
override suspend fun retrieveGroup(id: String): Group? {
val group = apiClient.getGroup(id)
group?.let { localRepository.saveGroup(it) }
retrieveGroupChat(id)
return group
}
override fun getGroup(id: String?) = id?.let { localRepository.getGroup(it) } ?: emptyFlow()
override fun getGroupFlowable(id: String?): Flowable<Group> = id?.let { localRepository.getGroupFlowable(it) } ?: Flowable.empty()
override fun leaveGroup(id: String?, keepChallenges: Boolean): Flowable<Group> {
override fun getGroup(id: String?): Flow<Group?> {
if (id?.isNotBlank() != true) {
return Flowable.empty()
return emptyFlow()
}
return apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges")
.doOnNext { localRepository.updateMembership(userID, id, false) }
.flatMapMaybe { localRepository.getGroupFlowable(id).firstElement() }
return localRepository.getGroup(id)
}
override fun joinGroup(id: String?): Flowable<Group> {
override suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group? {
if (id?.isNotBlank() != true) {
return Flowable.empty()
return null
}
return apiClient.joinGroup(id)
.doOnNext { group ->
localRepository.updateMembership(userID, id, true)
localRepository.save(group)
}
apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges")
localRepository.updateMembership(userID, id, false)
return localRepository.getGroup(id).firstOrNull()
}
override fun createGroup(
override suspend fun joinGroup(id: String?): Group? {
if (id?.isNotBlank() != true) {
return null
}
val group = apiClient.joinGroup(id)
group?.let {
localRepository.updateMembership(userID, id, true)
localRepository.save(group)
}
return group
}
override suspend fun createGroup(
name: String?,
description: String?,
leader: String?,
type: String?,
privacy: String?,
leaderCreateChallenge: Boolean?
): Flowable<Group> {
): Group? {
val group = Group()
group.name = name
group.description = description
group.type = type
group.leaderID = leader
group.privacy = privacy
return apiClient.createGroup(group).doOnNext {
localRepository.save(it)
}
val savedGroup = apiClient.createGroup(group)
savedGroup?.let { localRepository.save(it) }
return savedGroup
}
override fun updateGroup(
override suspend fun updateGroup(
group: Group?,
name: String?,
description: String?,
leader: String?,
leaderCreateChallenge: Boolean?
): Flowable<Group> {
): Group? {
if (group == null) {
return Flowable.empty()
return null
}
val copiedGroup = localRepository.getUnmanagedCopy(group)
copiedGroup.name = name
@ -224,21 +202,21 @@ class SocialRepositoryImpl(
}
}
override fun getGroups(type: String): Flowable<out List<Group>> = localRepository.getGroups(type)
override fun getGroups(type: String) = localRepository.getGroups(type)
override fun getPublicGuilds(): Flowable<out List<Group>> = localRepository.getPublicGuilds()
override fun getPublicGuilds() = localRepository.getPublicGuilds()
override fun getInboxConversations(): Flowable<out List<InboxConversation>> = localRepository.getInboxConversation(userID)
override fun getInboxConversations() = localRepository.getInboxConversation(userID)
override fun getInboxMessages(replyToUserID: String?): Flowable<out List<ChatMessage>> = localRepository.getInboxMessages(userID, replyToUserID)
override fun getInboxMessages(replyToUserID: String?) = localRepository.getInboxMessages(userID, replyToUserID)
override fun retrieveInboxMessages(uuid: String, page: Int): Flowable<List<ChatMessage>> {
return apiClient.retrieveInboxMessages(uuid, page).doOnNext { messages ->
messages.forEach {
it.isInboxMessage = true
}
localRepository.saveInboxMessages(userID, uuid, messages, page)
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? {
val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null
messages.forEach {
it.isInboxMessage = true
}
localRepository.saveInboxMessages(userID, uuid, messages, page)
return messages
}
override fun retrieveInboxConversations(): Flowable<List<InboxConversation>> {
@ -247,29 +225,32 @@ class SocialRepositoryImpl(
}
}
override fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): Flowable<List<ChatMessage>> {
return apiClient.postPrivateMessage(messageObject).flatMap { retrieveInboxMessages(recipientId, 0) }
override suspend fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): List<ChatMessage>? {
val message = apiClient.postPrivateMessage(messageObject)
return retrieveInboxMessages(recipientId, 0)
}
override fun postPrivateMessage(recipientId: String, message: String): Flowable<List<ChatMessage>> {
override suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>? {
val messageObject = HashMap<String, String>()
messageObject["message"] = message
messageObject["toUserId"] = recipientId
return postPrivateMessage(recipientId, messageObject)
}
override fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id)
override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
override fun retrieveGroupMembers(id: String, includeAllPublicFields: Boolean): Flowable<List<Member>> {
return apiClient.getGroupMembers(id, includeAllPublicFields)
.doOnNext { members -> localRepository.saveGroupMembers(id, members) }
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 fun inviteToGroup(id: String, inviteData: Map<String, Any>): Flowable<List<Void>> = apiClient.inviteToGroup(id, inviteData)
override fun getMember(userId: String?): Flowable<Member> {
override suspend fun retrieveMember(userId: String?): Member? {
return if (userId == null) {
Flowable.empty()
null
} else {
try {
apiClient.getMember(UUID.fromString(userId).toString())
@ -279,8 +260,8 @@ class SocialRepositoryImpl(
}
}
override fun getMemberWithUsername(username: String?): Flowable<Member> {
return getMember(username)
override suspend fun retrieveMemberWithUsername(username: String?): Member? {
return retrieveMember(username)
}
override fun findUsernames(username: String, context: String?, id: String?): Flowable<List<FindUsernameResult>> {
@ -315,7 +296,7 @@ class SocialRepositoryImpl(
}
}
override fun getUserGroups(type: String?): Flowable<out List<Group>> = localRepository.getUserGroups(userID, type)
override fun getUserGroups(type: String?) = localRepository.getUserGroups(userID, type)
override fun acceptQuest(user: User?, partyId: String): Flowable<Void> {
return apiClient.acceptQuest(partyId)

View file

@ -5,25 +5,26 @@ import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.local.TaskLocalRepository
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.interactors.ScoreTaskLocallyInteractor
import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.tasks.TaskList
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Single
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import java.text.SimpleDateFormat
import java.util.Date
@ -49,9 +50,10 @@ class TaskRepositoryImpl(
localRepository.saveTasks(userId, order, tasks)
}
override fun retrieveTasks(userId: String, tasksOrder: TasksOrder): Flowable<TaskList> {
return this.apiClient.tasks
.doOnNext { res -> this.localRepository.saveTasks(userId, tasksOrder, res) }
override suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList? {
val tasks = apiClient.getTasks() ?: return null
this.localRepository.saveTasks(userId, tasksOrder, tasks)
return tasks
}
override fun retrieveCompletedTodos(userId: String?): Flowable<TaskList> {
@ -69,13 +71,13 @@ class TaskRepositoryImpl(
}
@Suppress("ReturnCount")
override fun taskChecked(
override suspend fun taskChecked(
user: User?,
task: Task,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
): Flowable<TaskScoringResult> {
): TaskScoringResult? {
val localData = if (user != null && appConfigManager.enableLocalTaskScoring()) {
ScoreTaskLocallyInteractor.score(user, task, if (up) TaskDirection.UP else TaskDirection.DOWN)
} else null
@ -89,41 +91,33 @@ class TaskRepositoryImpl(
val now = Date().time
val id = task.id
if (lastTaskAction > now - 500 && !force || id == null) {
return Flowable.empty()
return null
}
lastTaskAction = now
return this.apiClient.postTaskDirection(id, (if (up) TaskDirection.UP else TaskDirection.DOWN).text)
.flatMapMaybe {
// There are cases where the user object is not set correctly. So the app refetches it as a fallback
if (user == null) {
localRepository.getUser(userID).firstElement()
} else {
Maybe.just(user)
}.map { user -> Pair(it, user) }
}
.map { (res, user): Pair<TaskDirectionData, User> ->
// save local task changes
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(userID).firstOrNull() ?: return null
// save local task changes
analyticsManager.logEvent(
"task_scored",
bundleOf(
Pair("type", task.type),
Pair("scored_up", up),
Pair("value", task.value)
)
)
if (res.lvl == 0) {
// Team tasks that require approval have weird data that we should just ignore.
return@map TaskScoringResult()
}
val result = TaskScoringResult(res, user.stats)
if (localData == null) {
notifyFunc?.invoke(result)
}
handleTaskResponse(user, res, task, up, localData?.delta ?: 0f)
result
}
analyticsManager.logEvent(
"task_scored",
bundleOf(
Pair("type", task.type),
Pair("scored_up", up),
Pair("value", task.value)
)
)
if (res.lvl == 0) {
// Team tasks that require approval have weird data that we should just ignore.
return TaskScoringResult()
}
val result = TaskScoringResult(res, thisUser.stats)
if (localData == null) {
notifyFunc?.invoke(result)
}
handleTaskResponse(thisUser, res, task, up, localData?.delta ?: 0f)
return result
}
override fun bulkScoreTasks(data: List<Map<String, String>>): Flowable<BulkTaskScoringData> {
@ -143,9 +137,13 @@ class TaskRepositoryImpl(
if (bgTask.type != TaskType.REWARD && (bgTask.value - localDelta) + res.delta != bgTask.value) {
bgTask.value = (bgTask.value - localDelta) + res.delta
if (TaskType.DAILY == bgTask.type || TaskType.TODO == bgTask.type) {
bgTask.completed = up
if (TaskType.DAILY == bgTask.type && up) {
bgTask.streak = (bgTask.streak ?: 0) + 1
bgTask.completeForUser(userID, up)
if (TaskType.DAILY == bgTask.type) {
if (up) {
bgTask.streak = (bgTask.streak ?: 0) + 1
} else {
bgTask.streak = (bgTask.streak ?: 0) - 1
}
}
} else if (TaskType.HABIT == bgTask.type) {
if (up) {
@ -190,31 +188,29 @@ class TaskRepositoryImpl(
}
}
override fun taskChecked(
override suspend fun taskChecked(
user: User?,
taskId: String,
up: Boolean,
force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?
): Maybe<TaskScoringResult> {
return localRepository.getTask(taskId).firstElement()
.flatMap { task -> taskChecked(user, task, up, force, notifyFunc).singleElement() }
): TaskScoringResult? {
val task = localRepository.getTask(taskId).firstOrNull() ?: return null
return taskChecked(user, task, up, force, notifyFunc)
}
override fun scoreChecklistItem(taskId: String, itemId: String): Flowable<Task> {
return apiClient.scoreChecklistItem(taskId, itemId)
.flatMapMaybe { localRepository.getTask(taskId).firstElement() }
.doOnNext { task ->
val updatedItem: ChecklistItem? = task.checklist?.lastOrNull { itemId == it.id }
if (updatedItem != null) {
localRepository.modify(updatedItem) { liveItem -> liveItem.completed = !liveItem.completed }
}
}
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? {
val task = apiClient.scoreChecklistItem(taskId, itemId)
val updatedItem: ChecklistItem? = task?.checklist?.lastOrNull { itemId == it.id }
if (updatedItem != null) {
localRepository.modify(updatedItem) { liveItem -> liveItem.completed = !liveItem.completed }
}
return task
}
override fun getTask(taskId: String): Flowable<Task> = localRepository.getTask(taskId)
override fun getTask(taskId: String) = localRepository.getTask(taskId)
override fun getTaskCopy(taskId: String): Flowable<Task> = localRepository.getTaskCopy(taskId)
override fun getTaskCopy(taskId: String) = localRepository.getTaskCopy(taskId)
override fun createTask(task: Task, force: Boolean): Flowable<Task> {
val now = Date().time
@ -305,15 +301,14 @@ class TaskRepositoryImpl(
.doOnSuccess { localRepository.updateTaskPositions(it) }
}
override fun getUnmanagedTask(taskid: String): Flowable<Task> =
getTask(taskid).map { localRepository.getUnmanagedCopy(it) }
override fun getUnmanagedTask(taskid: String) = getTask(taskid).map { localRepository.getUnmanagedCopy(it) }
override fun updateTaskInBackground(task: Task) {
updateTask(task).subscribe({ }, RxErrorHandler.handleEmptyError())
updateTask(task).subscribe({ }, ExceptionHandler.rx())
}
override fun createTaskInBackground(task: Task) {
createTask(task).subscribe({ }, RxErrorHandler.handleEmptyError())
createTask(task).subscribe({ }, ExceptionHandler.rx())
}
override fun getTaskCopies(userId: String): Flow<List<Task>> =

View file

@ -1,14 +1,12 @@
package com.habitrpg.android.habitica.data.implementation
import androidx.core.os.bundleOf
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.data.local.UserLocalRepository
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.android.habitica.extensions.filterMapEmpty
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
@ -17,9 +15,11 @@ import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.common.habitica.extensions.Optional
import com.habitrpg.shared.habitica.models.responses.TaskDirection
@ -28,7 +28,9 @@ import com.habitrpg.shared.habitica.models.tasks.Attribute
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.functions.BiFunction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import java.util.Date
import java.util.GregorianCalendar
import java.util.concurrent.TimeUnit
@ -70,45 +72,39 @@ class UserRepositoryImpl(
return updateUser(userID, key, value)
}
override fun retrieveUser(withTasks: Boolean): Flowable<User> =
retrieveUser(withTasks, false)
@Suppress("ReturnCount")
override fun retrieveUser(withTasks: Boolean, forced: Boolean, overrideExisting: Boolean): Flowable<User> {
override suspend fun retrieveUser(withTasks: Boolean, forced: Boolean, overrideExisting: Boolean): User? {
// Only retrieve again after 3 minutes or it's forced.
if (forced || this.lastSync == null || Date().time - (this.lastSync?.time ?: 0) > 180000) {
val user = apiClient.retrieveUser(withTasks) ?: return null
lastSync = Date()
return apiClient.retrieveUser(withTasks)
.doOnNext { localRepository.saveUser(it, overrideExisting) }
.doOnNext { user ->
if (withTasks) {
val id = user.id
val tasksOrder = user.tasksOrder
val tasks = user.tasks
if (id != null && tasksOrder != null && tasks != null) {
taskRepository.saveTasks(id, tasksOrder, tasks)
}
}
}
.flatMap { user ->
val calendar = GregorianCalendar()
val timeZone = calendar.timeZone
val offset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS)
if (offset.toInt() != (user.preferences?.timezoneOffset ?: 0)) {
return@flatMap updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString())
} else {
return@flatMap Flowable.just(user)
}
localRepository.saveUser(user)
if (withTasks) {
val id = user.id
val tasksOrder = user.tasksOrder
val tasks = user.tasks
if (id != null && tasksOrder != null && tasks != null) {
taskRepository.saveTasks(id, tasksOrder, tasks)
}
}
val calendar = GregorianCalendar()
val timeZone = calendar.timeZone
val offset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS)
/*if (offset.toInt() != user.preferences?.timezoneOffset ?: 0) {
return@flatMap updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString())
} else {
return@flatMap Flowable.just(user)
}*/
return user
} else {
return localRepository.getUserFlowable(userID).take(1)
return null
}
}
override fun revive(): Flowable<User> = zipWithLiveUser(apiClient.revive()) { newUser, user ->
mergeUser(user, newUser)
override suspend fun revive(): User? {
apiClient.revive()
return retrieveUser(false, true)
}
.flatMap { retrieveUser(false, true) }
override fun resetTutorial(): Maybe<User> {
return localRepository.getTutorialSteps()
@ -123,9 +119,13 @@ class UserRepositoryImpl(
.flatMap { updateData -> updateUser(updateData).firstElement() }
}
override fun sleep(user: User): Flowable<User> {
localRepository.modify(user) { it.preferences?.sleep = !(it.preferences?.sleep ?: false) }
return apiClient.sleep().map { user }
override suspend fun sleep(user: User): User {
val newValue = !(user.preferences?.sleep ?: false)
localRepository.modify(user) { it.preferences?.sleep = newValue }
if (apiClient.sleep() != true) {
localRepository.modify(user) { it.preferences?.sleep = !newValue }
}
return user
}
override fun getSkills(user: User): Flowable<out List<Skill>> =
@ -137,7 +137,7 @@ class UserRepositoryImpl(
override fun useSkill(key: String, target: String?, taskId: String): Flowable<SkillResponse> {
return zipWithLiveUser(apiClient.useSkill(key, target ?: "", taskId)) { response, user ->
response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0)
response.expDiff = (response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.expDiff =(response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0)
response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f)
response.user?.let { mergeUser(user, it) }
@ -148,7 +148,7 @@ class UserRepositoryImpl(
override fun useSkill(key: String, target: String?): Flowable<SkillResponse> {
return zipWithLiveUser(apiClient.useSkill(key, target ?: "")) { response, user ->
response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0)
response.expDiff = (response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.expDiff =(response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0)
response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f)
response.user?.let { mergeUser(user, it) }
@ -156,12 +156,16 @@ class UserRepositoryImpl(
}
}
override fun changeClass(): Flowable<User> = apiClient.changeClass().flatMap { retrieveUser(withTasks = false, forced = true) }
override suspend fun disableClasses(): User? = apiClient.disableClasses()
override fun disableClasses(): Flowable<User> = apiClient.disableClasses().flatMap { retrieveUser(withTasks = false, forced = true) }
override suspend fun changeClass(selectedClass: String?): User? {
apiClient.changeClass(selectedClass)
return retrieveUser(false, forced = true)
}
override fun changeClass(selectedClass: String): Flowable<User> = apiClient.changeClass(selectedClass)
.flatMap { retrieveUser(false, forced = true) }
override fun unlockPath(customization: Customization): Flowable<UnlockResponse> {
return unlockPath(customization.path, customization.price ?: 0)
}
override fun unlockPath(path: String, price: Int): Flowable<UnlockResponse> {
return zipWithLiveUser(apiClient.unlockPath(path)) { unlockResponse, copiedUser ->
@ -175,11 +179,7 @@ class UserRepositoryImpl(
}
}
override fun unlockPath(customization: Customization): Flowable<UnlockResponse> {
return unlockPath(customization.unlockPath, customization.price ?: 0)
}
override fun runCron() {
override suspend fun runCron() {
runCron(ArrayList())
}
@ -188,9 +188,8 @@ class UserRepositoryImpl(
return localRepository.getUserQuestStatus(userID)
}
override fun reroll(): Flowable<User> {
override suspend fun reroll(): User? {
return apiClient.reroll()
.flatMap { retrieveUser(true, true, true) }
}
override fun readNotifications(notificationIds: Map<String, List<String>>): Flowable<List<Any>> =
@ -210,8 +209,9 @@ class UserRepositoryImpl(
.doOnNext { apiClient.setLanguageCode(languageCode) }
}
override fun resetAccount(): Flowable<User> {
return apiClient.resetAccount().flatMap { retrieveUser(withTasks = true, forced = true) }
override suspend fun resetAccount(): User? {
apiClient.resetAccount()
return retrieveUser(withTasks = true, forced = true)
}
override fun deleteAccount(password: String): Flowable<Void> =
@ -262,7 +262,7 @@ class UserRepositoryImpl(
liveUser.stats?.points = liveUser.stats?.points?.dec()
}
},
RxErrorHandler.handleEmptyError()
ExceptionHandler.rx()
)
return zipWithLiveUser(apiClient.allocatePoint(stat.value)) { stats, user ->
localRepository.modify(user) { liveUser ->
@ -295,26 +295,33 @@ class UserRepositoryImpl(
stats
}
override fun runCron(tasks: MutableList<Task>) {
var observable: Maybe<Any> = localRepository.getUserFlowable(userID).firstElement()
.filter { it.needsCron }
.map { user ->
localRepository.modify(user) { liveUser ->
liveUser.needsCron = false
liveUser.lastCron = Date()
override suspend fun runCron(tasks: MutableList<Task>) {
withContext(Dispatchers.Main) {
var observable: Maybe<Any> = localRepository.getUserFlowable(userID).firstElement()
.filter { it.needsCron }
.map { user ->
localRepository.modify(user) { liveUser ->
liveUser.needsCron = false
liveUser.lastCron = Date()
}
user
}
user
if (tasks.isNotEmpty()) {
val scoringList = mutableListOf<Map<String, String>>()
for (task in tasks) {
val map = mutableMapOf<String, String>()
map["id"] = task.id ?: ""
map["direction"] = TaskDirection.UP.text
scoringList.add(map)
}
observable = observable.flatMap { taskRepository.bulkScoreTasks(scoringList).firstElement() }
}
if (tasks.isNotEmpty()) {
val scoringList = tasks.map { mapOf(Pair("id", it.id ?: ""), Pair("direction", TaskDirection.UP.text)) }
observable = observable.flatMap { taskRepository.bulkScoreTasks(scoringList).firstElement() }
observable.flatMap { apiClient.runCron().firstElement() }
// .flatMap {
// this.retrieveUser(withTasks = true, forced = true)
// }
.subscribe({ }, ExceptionHandler.rx())
}
observable.flatMap { apiClient.runCron().firstElement() }
.flatMap { this.retrieveUser(withTasks = true, forced = true).firstElement() }
.subscribe({ }, {
analyticsManager.logEvent("cron failed", bundleOf(Pair("error", it.localizedMessage)))
RxErrorHandler.reportError(it)
})
}
override fun useCustomization(type: String, category: String?, identifier: String): Flowable<User> {
@ -340,7 +347,7 @@ class UserRepositoryImpl(
}
}
},
RxErrorHandler.handleEmptyError()
ExceptionHandler.rx()
)
}
var updatePath = "preferences.$type"
@ -375,22 +382,19 @@ class UserRepositoryImpl(
return localRepository.getTeamPlans(userID)
}
override fun retrieveTeamPlan(teamID: String): Flowable<Group> {
return Flowable.zip(
apiClient.getGroup(teamID), apiClient.getTeamPlanTasks(teamID)
) { team, tasks ->
team.tasks = tasks
team
override suspend fun retrieveTeamPlan(teamID: String): Group? {
val team = apiClient.getGroup(teamID) ?: return null
val tasks = apiClient.getTeamPlanTasks(teamID)
localRepository.save(team)
val id = team.id
val tasksOrder = team.tasksOrder
if (id.isNotBlank() && tasksOrder != null && tasks != null) {
taskRepository.saveTasks(id, tasksOrder, tasks)
}
.doOnNext { localRepository.save(it) }
.doOnNext { team ->
val id = team.id
val tasksOrder = team.tasksOrder
val tasks = team.tasks
if (id.isNotBlank() && tasksOrder != null && tasks != null) {
taskRepository.saveTasks(id, tasksOrder, tasks)
}
}
val members = apiClient.getGroupMembers(teamID, true) ?: return team
localRepository.save(members.map { it.id?.let { member -> GroupMembership(member, id) } }.filterNotNull())
members.let { localRepository.save(members) }
return team
}
override fun getTeamPlan(teamID: String): Flowable<Group> {

View file

@ -28,7 +28,7 @@ interface InventoryLocalRepository : ContentLocalRepository {
fun getOwnedPets(userID: String): Flow<List<OwnedPet>>
fun getInAppRewards(): Flowable<out List<ShopItem>>
fun getQuestContent(key: String): Flowable<QuestContent>
fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flowable<out List<Equipment>>

View file

@ -7,29 +7,30 @@ import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.core.Flowable
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface SocialLocalRepository : BaseLocalRepository {
fun getPublicGuilds(): Flowable<out List<Group>>
fun getUserGroups(userID: String, type: String?): Flowable<out List<Group>>
fun getUserGroups(userID: String, type: String?): Flow<List<Group>>
fun getGroups(type: String): Flowable<out List<Group>>
fun getGroup(id: String): Flow<Group?>
fun getGroupFlowable(id: String): Flowable<Group>
fun saveGroup(group: Group)
fun getGroupChat(groupId: String): Flowable<out List<ChatMessage>>
fun deleteMessage(id: String)
fun getGroupMembers(partyId: String): Flow<List<Member>>
fun getPartyMembers(partyId: String): Flow<List<Member>>
fun getGroupMembers(groupID: String): Flow<List<Member>>
fun updateRSVPNeeded(user: User?, newValue: Boolean)
fun likeMessage(chatMessage: ChatMessage, userId: String, liked: Boolean)
fun saveGroupMembers(groupId: String?, members: List<Member>)
fun savePartyMembers(groupId: String?, members: List<Member>)
fun removeQuest(partyId: String)
@ -39,13 +40,13 @@ interface SocialLocalRepository : BaseLocalRepository {
fun doesGroupExist(id: String): Boolean
fun updateMembership(userId: String, id: String, isMember: Boolean)
fun getGroupMembership(userId: String, id: String): Flowable<GroupMembership>
fun getGroupMembership(userId: String, id: String): Flow<GroupMembership?>
fun getGroupMemberships(userId: String): Flowable<out List<GroupMembership>>
fun rejectGroupInvitation(userID: String, groupID: String)
fun getInboxMessages(userId: String, replyToUserID: String?): Flowable<out List<ChatMessage>>
fun getInboxMessages(userId: String, replyToUserID: String?): Flow<RealmResults<ChatMessage>>
fun getInboxConversation(userId: String): Flowable<out List<InboxConversation>>
fun getInboxConversation(userId: String): Flow<RealmResults<InboxConversation>>
fun saveGroupMemberships(userID: String?, memberships: List<GroupMembership>)
fun saveInboxMessages(
userID: String,

View file

@ -19,8 +19,8 @@ interface TaskLocalRepository : BaseLocalRepository {
fun deleteTask(taskID: String)
fun getTask(taskId: String): Flowable<Task>
fun getTaskCopy(taskId: String): Flowable<Task>
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task>
fun markTaskCompleted(taskId: String, isCompleted: Boolean)
@ -33,6 +33,7 @@ interface TaskLocalRepository : BaseLocalRepository {
fun updateTaskPositions(taskOrder: List<String>)
fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>)
fun getErroredTasks(userID: String): Flowable<out List<Task>>
fun getUser(userID: String): Flowable<User>
fun getUserFlowable(userID: String): Flowable<User>
fun getUser(userID: String): Flow<User>
fun getTasksForChallenge(challengeID: String?, userID: String?): Flowable<out List<Task>>
}

View file

@ -1,7 +1,7 @@
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.InventoryLocalRepository
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food
@ -39,14 +39,12 @@ class RealmInventoryLocalRepository(realm: Realm) : RealmContentLocalRepository(
.filter { it.isLoaded }
}
override fun getQuestContent(key: String): Flowable<QuestContent> {
return RxJavaBridge.toV3Flowable(
realm.where(QuestContent::class.java).equalTo("key", key)
override fun getQuestContent(key: String): Flow<QuestContent?> {
return realm.where(QuestContent::class.java).equalTo("key", key)
.findAll()
.asFlowable()
.toFlow()
.filter { content -> content.isLoaded && content.isValid && !content.isEmpty() }
.map { content -> content.first() }
)
}
override fun getEquipment(searchedKeys: List<String>): Flowable<out List<Equipment>> {
@ -240,7 +238,7 @@ class RealmInventoryLocalRepository(realm: Realm) : RealmContentLocalRepository(
}
override fun changeOwnedCount(type: String, key: String, userID: String, amountToAdd: Int) {
getOwnedItem(userID, type, key, true).firstElement().subscribe({ changeOwnedCount(it, amountToAdd) }, RxErrorHandler.handleEmptyError())
getOwnedItem(userID, type, key, true).firstElement().subscribe({ changeOwnedCount(it, amountToAdd) }, ExceptionHandler.rx())
}
override fun changeOwnedCount(item: OwnedItem, amountToAdd: Int?) {

View file

@ -14,21 +14,21 @@ import io.reactivex.rxjava3.core.Flowable
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.toFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), SocialLocalRepository {
override fun getGroupMembership(userId: String, id: String): Flowable<GroupMembership> = RxJavaBridge.toV3Flowable(
realm.where(GroupMembership::class.java)
override fun getGroupMembership(userId: String, id: String) = realm.where(GroupMembership::class.java)
.equalTo("userID", userId)
.equalTo("groupID", id)
.findAll()
.asFlowable()
.toFlow()
.filter { it.isLoaded && it.isNotEmpty() }
.map { it.first() }
)
override fun getGroupMemberships(userId: String): Flowable<out List<GroupMembership>> = RxJavaBridge.toV3Flowable(
realm.where(GroupMembership::class.java)
@ -134,29 +134,25 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
.filter { it.isLoaded }
)
override fun getUserGroups(userID: String, type: String?): Flowable<out List<Group>> = RxJavaBridge.toV3Flowable(
realm.where(GroupMembership::class.java)
@OptIn(ExperimentalCoroutinesApi::class)
override fun getUserGroups(userID: String, type: String?) = realm.where(GroupMembership::class.java)
.equalTo("userID", userID)
.findAll()
.asFlowable()
.filter { it.isLoaded }
)
.flatMap { memberships ->
RxJavaBridge.toV3Flowable(
realm.where(Group::class.java)
.equalTo("type", type ?: "guild")
.notEqualTo("id", Group.TAVERN_ID)
.`in`(
"id",
memberships.map {
return@map it.groupID
}.toTypedArray()
)
.sort("memberCount", Sort.DESCENDING)
.findAll()
.asFlowable()
.filter { it.isLoaded }
)
.toFlow()
.filter { it.isLoaded }
.flatMapLatest { memberships ->
realm.where(Group::class.java)
.equalTo("type", type ?: "guild")
.notEqualTo("id", Group.TAVERN_ID)
.`in`(
"id",
memberships.map {
return@map it.groupID
}.toTypedArray()
)
.sort("memberCount", Sort.DESCENDING)
.findAll()
.toFlow()
}
override fun getGroups(type: String): Flowable<out List<Group>> {
@ -169,17 +165,6 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
)
}
override fun getGroupFlowable(id: String): Flowable<Group> {
return RxJavaBridge.toV3Flowable(
realm.where(Group::class.java)
.equalTo("id", id)
.findAll()
.asFlowable()
.filter { group -> group.isLoaded && group.isValid && !group.isEmpty() }
.map { groups -> groups.first() }
)
}
override fun getGroup(id: String): Flow<Group?> {
return realm.where(Group::class.java)
.equalTo("id", id)
@ -205,13 +190,17 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
executeTransaction { chatMessage?.deleteFromRealm() }
}
override fun getGroupMembers(partyId: String): Flow<List<Member>> {
return realm.where(Member::class.java)
override fun getPartyMembers(partyId: String) = realm.where(Member::class.java)
.equalTo("party.id", partyId)
.findAll()
.toFlow()
.filter { it.isLoaded }
}
.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) {
executeTransaction { user?.party?.quest?.RSVPNeeded = newValue }
@ -239,7 +228,7 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
}
}
override fun saveGroupMembers(groupId: String?, members: List<Member>) {
override fun savePartyMembers(groupId: String?, members: List<Member>) {
saveSyncronous(members)
if (groupId != null) {
val existingMembers = realm.where(Member::class.java).equalTo("party.id", groupId).findAll()
@ -302,27 +291,19 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
return party != null && party.isValid
}
override fun getInboxMessages(userId: String, replyToUserID: String?): Flowable<out List<ChatMessage>> {
return RxJavaBridge.toV3Flowable(
realm.where(ChatMessage::class.java)
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()
.asFlowable()
.toFlow()
.filter { it.isLoaded }
)
}
override fun getInboxConversation(userId: String): Flowable<out List<InboxConversation>> {
return RxJavaBridge.toV3Flowable(
realm.where(InboxConversation::class.java)
override fun getInboxConversation(userId: String) = realm.where(InboxConversation::class.java)
.equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING)
.findAll()
.asFlowable()
.toFlow()
.filter { it.isLoaded }
)
}
}

View file

@ -12,42 +12,55 @@ import hu.akarnokd.rxjava3.bridge.RxJavaBridge
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.realm.Realm
import io.realm.RealmResults
import io.realm.Sort
import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TaskLocalRepository {
override fun getTasks(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow()
return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value)
.beginGroup()
.equalTo("userId", userID)
.or()
.`in`("group.groupID", includedGroupIDs)
.endGroup()
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
.findAll()
return findTasks(taskType, userID, includedGroupIDs)
.toFlow()
.filter { it.isLoaded }
}
override fun getTasksFlowable(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flowable<out List<Task>> {
if (realm.isClosed) return Flowable.empty()
return RxJavaBridge.toV3Flowable(realm.where(Task::class.java)
return RxJavaBridge.toV3Flowable(findTasks(taskType, userID, includedGroupIDs)
.asFlowable()
.filter { it.isLoaded })
}
private fun findTasks(
taskType: TaskType,
ownerID: String,
includedGroupIDs: Array<String>
): RealmResults<Task> {
return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value)
.beginGroup()
.equalTo("userId", userID)
.equalTo("userId", ownerID)
.or()
.beginGroup()
.`in`("group.groupID", includedGroupIDs)
.and()
.beginGroup()
.contains("group.assignedUsers", ownerID)
.or()
.isEmpty("group.assignedUsers")
.endGroup()
.endGroup()
.or()
.equalTo("group.groupID", ownerID)
.endGroup()
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
.findAll()
.asFlowable()
.filter { it.isLoaded })
}
override fun getTasks(userId: String): Flow<List<Task>> {
@ -126,14 +139,13 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
return taskList
}
private fun removeOldTasks(userID: String, onlineTaskList: List<Task>) {
val groupIDs = onlineTaskList.map { it.group?.groupID }.distinct().toTypedArray()
private fun removeOldTasks(ownerID: String, onlineTaskList: List<Task>) {
if (realm.isClosed) return
val localTasks = realm.where(Task::class.java)
.beginGroup()
.equalTo("userId", userID)
.equalTo("userId", ownerID)
.or()
.`in`("group.groupID", groupIDs)
.equalTo("group.groupID", ownerID)
.endGroup()
.beginGroup()
.beginGroup()
@ -177,19 +189,17 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
}
}
override fun getTask(taskId: String): Flowable<Task> {
override fun getTask(taskId: String): Flow<Task> {
if (realm.isClosed) {
return Flowable.empty()
return emptyFlow()
}
return RxJavaBridge.toV3Flowable(
realm.where(Task::class.java).equalTo("id", taskId).findAll().asFlowable()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.cast(Task::class.java)
)
return realm.where(Task::class.java).equalTo("id", taskId).findAll().toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.map { it.first() }
.filterNotNull()
}
override fun getTaskCopy(taskId: String): Flowable<Task> {
override fun getTaskCopy(taskId: String): Flow<Task> {
return getTask(taskId)
.map { task ->
return@map if (task.isManaged && task.isValid) {
@ -260,7 +270,7 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
).retry(1)
}
override fun getUser(userID: String): Flowable<User> {
override fun getUserFlowable(userID: String): Flowable<User> {
return RxJavaBridge.toV3Flowable(
realm.where(User::class.java)
.equalTo("id", userID)
@ -271,6 +281,16 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
)
}
override fun getUser(userID: String): Flow<User> {
return realm.where(User::class.java)
.equalTo("id", userID)
.findAll()
.toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isValid && !realmObject.isEmpty() }
.map { users -> users.first() }
.filterNotNull()
}
override fun getTasksForChallenge(challengeID: String?, userID: String?): Flowable<out List<Task>> {
return RxJavaBridge.toV3Flowable(
realm.where(Task::class.java)

View file

@ -1,10 +1,10 @@
package com.habitrpg.android.habitica.extensions
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.functions.Consumer
fun <T : Any> Flowable<T>.subscribeWithErrorHandler(function: Consumer<T>): Disposable {
return subscribe(function, RxErrorHandler.handleEmptyError())
return subscribe(function, ExceptionHandler.rx())
}

View file

@ -18,12 +18,16 @@ class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.comm
private var worldState: WorldState? = null
init {
contentRepository?.getWorldState()?.subscribe(
{
worldState = it
},
RxErrorHandler.handleEmptyError()
)
try {
contentRepository?.getWorldState()?.subscribe(
{
worldState = it
},
ExceptionHandler.rx()
)
} catch (_: java.lang.IllegalStateException) {
// pass
}
}
private val remoteConfig = FirebaseRemoteConfig.getInstance()
@ -137,9 +141,7 @@ class AppConfigManager(contentRepository: ContentRepository?): com.habitrpg.comm
}
fun enableTeamBoards(): Boolean {
if (BuildConfig.DEBUG) {
return true
}
return true
return remoteConfig.getBoolean("enableTeamBoards")
}

View file

@ -4,24 +4,31 @@ import android.util.Log
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import io.reactivex.rxjava3.functions.Consumer
import java.io.EOFException
import java.io.IOException
import kotlinx.coroutines.CoroutineExceptionHandler
import okhttp3.internal.http2.ConnectionShutdownException
import retrofit2.HttpException
import java.io.EOFException
import java.io.IOException
class RxErrorHandler {
class ExceptionHandler {
private var analyticsManager: AnalyticsManager? = null
companion object {
private var instance: RxErrorHandler? = null
private var instance = ExceptionHandler()
fun init(analyticsManager: AnalyticsManager) {
instance = RxErrorHandler()
instance?.analyticsManager = analyticsManager
instance.analyticsManager = analyticsManager
}
fun handleEmptyError(): Consumer<Throwable> {
fun coroutine(handler: ((Throwable) -> Unit)? = null): CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, throwable ->
reportError(throwable)
handler?.invoke(throwable)
}
}
fun rx(): Consumer<Throwable> {
// Can't be turned into a lambda, because it then doesn't work for some reason.
return Consumer { reportError(it) }
}
@ -40,7 +47,7 @@ class RxErrorHandler {
!retrofit2.adapter.rxjava3.HttpException::class.java.isAssignableFrom(throwable.javaClass) &&
throwable !is ConnectionShutdownException
) {
instance?.analyticsManager?.logException(throwable)
instance.analyticsManager?.logException(throwable)
}
}
}

View file

@ -0,0 +1,10 @@
package com.habitrpg.android.habitica.helpers
import android.content.res.Resources
import com.habitrpg.android.habitica.models.tasks.Task
interface GroupPlanInfoProvider {
fun assignedTextForTask(resources: Resources, assignedUsers: List<String>): String
fun canScoreTask(task: Task): Boolean
fun canEditTask(task: Task): Boolean
}

View file

@ -120,6 +120,6 @@ class MainNotificationsManager: NotificationsManager {
private fun readNotification(notification: Notification) {
apiClient?.get()?.readNotification(notification.id)
?.subscribe({ }, RxErrorHandler.handleEmptyError())
?.subscribe({ }, ExceptionHandler.rx())
}
}

View file

@ -25,26 +25,25 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.extensions.addOkButton
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.common.habitica.models.IAPGift
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.Transaction
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.android.habitica.ui.activities.PurchaseActivity
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.rxjava3.core.Flowable
import java.util.Date
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import com.habitrpg.common.habitica.models.IAPGift
import com.habitrpg.common.habitica.models.PurchaseValidationRequest
import com.habitrpg.common.habitica.models.Transaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import retrofit2.HttpException
import java.util.Date
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class PurchaseHandler(
private val context: Context,
@ -88,7 +87,7 @@ class PurchaseHandler(
return
}
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
for (purchase in purchases) {
consume(purchase)
}
@ -186,7 +185,8 @@ class PurchaseHandler(
return skuDetailsResult.skuDetailsList
}
fun purchase(activity: Activity, skuDetails: SkuDetails, recipient: String? = null) {
fun purchase(activity: Activity, skuDetails: SkuDetails, recipient: String? = null, isSaleGemPurchase: Boolean = false) {
this.isSaleGemPurchase = isSaleGemPurchase
recipient?.let {
addGift(skuDetails.sku, it)
}
@ -219,7 +219,7 @@ class PurchaseHandler(
apiClient.validatePurchase(validationRequest).subscribe({
processedPurchase(purchase)
val gift = removeGift(sku)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
consume(purchase)
}
displayConfirmationDialog(purchase, gift?.second)
@ -232,7 +232,7 @@ class PurchaseHandler(
apiClient.validateNoRenewSubscription(validationRequest).subscribe({
processedPurchase(purchase)
val gift = removeGift(sku)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
consume(purchase)
}
displayConfirmationDialog(purchase, gift?.second)
@ -245,7 +245,7 @@ class PurchaseHandler(
apiClient.validateSubscription(validationRequest).subscribe({
processedPurchase(purchase)
analyticsManager.logEvent("user_subscribed", bundleOf(Pair("sku", sku)))
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
acknowledgePurchase(purchase)
}
displayConfirmationDialog(purchase)
@ -268,7 +268,9 @@ class PurchaseHandler(
}
private fun processedPurchase(purchase: Purchase) {
userViewModel.userRepository.retrieveUser(false, true).subscribeWithErrorHandler {}
MainScope().launch(ExceptionHandler.coroutine()) {
userViewModel.userRepository.retrieveUser(false, true)
}
}
private fun buildValidationRequest(purchase: Purchase): PurchaseValidationRequest {
@ -296,7 +298,7 @@ class PurchaseHandler(
if (res.message != null && res.message == "RECEIPT_ALREADY_USED") {
processedPurchase(purchase)
removeGift(purchase.skus.firstOrNull())
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch(ExceptionHandler.coroutine()) {
consume(purchase)
}
return
@ -331,9 +333,9 @@ class PurchaseHandler(
return fallback
}
fun cancelSubscription(): Flowable<User> {
return apiClient.cancelSubscription()
.flatMap { userViewModel.userRepository.retrieveUser(false, true) }
suspend fun cancelSubscription(): User? {
apiClient.cancelSubscription()
return userViewModel.userRepository.retrieveUser(false, true)
}
private fun durationString(sku: String): String {
@ -346,18 +348,31 @@ class PurchaseHandler(
}
}
private var isSaleGemPurchase = false
private fun gemAmountString(sku: String): String {
return when (sku) {
PurchaseTypes.Purchase4Gems -> "4"
PurchaseTypes.Purchase21Gems -> "21"
PurchaseTypes.Purchase42Gems -> "42"
PurchaseTypes.Purchase84Gems -> "84"
else -> ""
if (isSaleGemPurchase) {
isSaleGemPurchase = false
return when (sku) {
PurchaseTypes.Purchase4Gems -> "5"
PurchaseTypes.Purchase21Gems -> "30"
PurchaseTypes.Purchase42Gems -> "60"
PurchaseTypes.Purchase84Gems -> "125"
else -> ""
}
} else {
return when (sku) {
PurchaseTypes.Purchase4Gems -> "4"
PurchaseTypes.Purchase21Gems -> "21"
PurchaseTypes.Purchase42Gems -> "42"
PurchaseTypes.Purchase84Gems -> "84"
else -> ""
}
}
}
private fun displayConfirmationDialog(purchase: Purchase, giftedTo: String? = null) {
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.Main).launch(ExceptionHandler.coroutine()) {
val application = (context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launch
val sku = purchase.skus.firstOrNull() ?: return@launch

View file

@ -37,7 +37,7 @@ class SoundFile(val theme: String, private val fileName: String) {
player?.setDataSource(file?.path)
val attributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_NOTIFICATION)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build()
player?.setAudioAttributes(attributes)
player?.prepare()
@ -48,7 +48,7 @@ class SoundFile(val theme: String, private val fileName: String) {
player?.start()
} catch (e: IllegalStateException) {
} catch (e: Exception) {
RxErrorHandler.reportError(e)
ExceptionHandler.reportError(e)
}
}
}

View file

@ -34,7 +34,7 @@ class SoundManager {
soundFiles.add(SoundFile(soundTheme, SoundReward))
soundFiles.add(SoundFile(soundTheme, SoundTodo))
soundFileLoader.download(soundFiles)
.subscribe({}, RxErrorHandler.handleEmptyError())
.subscribe({}, ExceptionHandler.rx())
}
fun loadAndPlayAudio(type: String) {
@ -54,7 +54,7 @@ class SoundManager {
loadedSoundFiles[type] = file
file.play()
},
RxErrorHandler.handleEmptyError()
ExceptionHandler.rx()
)
}
}

View file

@ -16,7 +16,11 @@ import com.habitrpg.android.habitica.receivers.TaskReceiver
import com.habitrpg.shared.habitica.HLogger
import com.habitrpg.shared.habitica.LogLevel
import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@ -55,10 +59,12 @@ class TaskAlarmManager(
// We currently only use this function to schedule the next reminder for dailies
// We may be able to use repeating alarms instead of this in the future
fun addAlarmForTaskId(taskId: String) {
taskRepository.getTaskCopy(taskId)
.filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type }
.firstElement()
.subscribe({ this.setAlarmsForTask(it) }, RxErrorHandler.handleEmptyError())
MainScope().launch(ExceptionHandler.coroutine()) {
val task = taskRepository.getTaskCopy(taskId)
.filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type }
.first()
setAlarmsForTask(task)
}
}
suspend fun scheduleAllSavedAlarms(preventDailyReminder: Boolean) {
@ -216,7 +222,11 @@ class TaskAlarmManager(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 60000, pendingIntent)
} else {
alarmManager?.setAlarmClock(AlarmClockInfo(time, pendingIntent), pendingIntent)
try {
alarmManager?.setAlarmClock(AlarmClockInfo(time, pendingIntent), pendingIntent)
} catch (e: SecurityException) {
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 60000, pendingIntent)
}
}
}
}

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