Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f0ec178
- Create DataSyncKey, DataSyncIdentifier, and DataSyncValue wrappers
ariel10aguero Dec 31, 2025
c25fb96
- Create DataSync concept module on repositories
ariel10aguero Jan 5, 2026
d98feb8
add proguard
ariel10aguero Jan 5, 2026
19a6612
- Create DataSync module on repositories
ariel10aguero Jan 9, 2026
15537db
- Add NetworkQueryMemeServer and AuthenticationStorage injection to D…
ariel10aguero Jan 9, 2026
b33fd64
Remove inject declaration
ariel10aguero Jan 9, 2026
42d1680
- Refactor DataSyncManager to follow ConnectManager listener architec…
ariel10aguero Jan 13, 2026
a93ce3d
- Refactor DataSyncKey from inline value class to sealed class
ariel10aguero Jan 13, 2026
4c6e55f
- Add startDataSyncObservation method to DataSyncRepository interface
ariel10aguero Jan 14, 2026
579f1d1
- Fix savePrivatePhoto to pass value instead the object
ariel10aguero Jan 14, 2026
78f0918
- Review NetworkQueryMemeServerImpl for AES256JNCryptorOutputStream u…
ariel10aguero Jan 15, 2026
2593798
- Implement applyMergedItemsToLocal to update local DB with newer ser…
ariel10aguero Jan 19, 2026
bb37b5d
- Implement saveFeedStatus and saveFeedItemStatus methods in DataSync…
ariel10aguero Jan 22, 2026
9299055
- Implement updateTimezoneEnabledStatus in SphinxRepository to save t…
ariel10aguero Jan 26, 2026
50614e9
- Add onApplySyncedData method to DataSyncManagerListener interface
ariel10aguero Jan 27, 2026
65c8c9d
Fix high-priority performance issues in SphinxRepository
tomastiminskas Jan 28, 2026
f13d906
Fix medium and low priority performance issues in SphinxRepository
tomastiminskas Jan 28, 2026
37caeb6
Add database indexes for performance optimization (migration 30)
tomastiminskas Jan 28, 2026
f5e2aec
- Modify getDataSyncFile to use networkRelayCall.getRawJson instead a…
ariel10aguero Jan 29, 2026
623713e
More performance improvements and fixes
tomastiminskas Jan 29, 2026
993bf5b
- Remove all the queries to local tables to compare values on sync pr…
ariel10aguero Jan 30, 2026
ea78f7e
- Create new account to test data sync
ariel10aguero Feb 3, 2026
a04e9b8
- Add syncWithServer on DataSyncRepository
ariel10aguero Feb 4, 2026
19c53bd
- Handle null item_id on syncFeedSubscriptionStatus using getPodcastById
ariel10aguero Feb 4, 2026
810b78f
- Debug race condition in saveDataSyncItem triggering syncWithServer …
ariel10aguero Feb 5, 2026
8da63cf
- Merge tt/fix/performance-high into aa/feature/sync-data branch
ariel10aguero Feb 11, 2026
49a15ca
Merge branch 'master' into aa/feature/sync-data
tomastiminskas Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,7 @@ include ':sphinx:application:common:highlighting-tool'
include ':sphinx:activity:call-activity'
include ':sphinx:activity:concepts:concept-grapheneos-manager'
include ':sphinx:activity:features:grapheneos-manager'
include ':sphinx:application:data:concepts:repositories:concept-repository-data-sync'
include ':sphinx:application:data:concepts:concept-data-sync'
include ':sphinx:application:data:features:crypto:feature-data-sync'
include ':sphinx:application:data:features:feature-data-sync'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package chat.sphinx.wrapper_common.datasync

import chat.sphinx.wrapper_common.DateTime

data class DataSync(
val sync_key: DataSyncKey,
val identifier: DataSyncIdentifier,
val date: DateTime,
val sync_value: DataSyncValue
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package chat.sphinx.wrapper_common.datasync

import com.squareup.moshi.Moshi
import com.squareup.moshi.JsonDataException

// Convert raw response to processed items
fun ItemsResponseRaw.toSettingItems(): List<SettingItem> {
return items.map { raw ->
SettingItem(
key = raw.key,
identifier = raw.identifier,
date = raw.date,
value = DataSyncJson.fromAny(raw.value)
)
}
}

// Convert processed items back to raw (for uploading)
fun List<SettingItem>.toItemsResponseRaw(): ItemsResponseRaw {
return ItemsResponseRaw(
items = this.map { item ->
SettingItemRaw(
key = item.key,
identifier = item.identifier,
date = item.date,
value = when (val v = item.value) {
is DataSyncJson.StringValue -> v.value
is DataSyncJson.ObjectValue -> v.value
}
)
}
)
}

// Parse JSON string to raw response
@Throws(JsonDataException::class)
fun String.toItemsResponseRaw(moshi: Moshi): ItemsResponseRaw {
val adapter = moshi.adapter(ItemsResponseRaw::class.java)
return adapter.fromJson(this)
?: throw IllegalArgumentException("Invalid JSON for ItemsResponse")
}

fun String.toItemsResponseRawNull(moshi: Moshi): ItemsResponseRaw? {
return try {
this.toItemsResponseRaw(moshi)
} catch (e: Exception) {
null
}
}

// Serialize raw response to JSON
fun ItemsResponseRaw.toJson(moshi: Moshi): String {
val adapter = moshi.adapter(ItemsResponseRaw::class.java)
return adapter.toJson(this)
}

// Serialize processed items to JSON (via raw)
fun List<SettingItem>.toJson(moshi: Moshi): String {
return this.toItemsResponseRaw().toJson(moshi)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package chat.sphinx.wrapper_common.datasync

@JvmInline
value class DataSyncIdentifier(val value: String) {
init {
require(value.isNotEmpty()) {
"DataSyncIdentifier cannot be empty"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package chat.sphinx.wrapper_common.datasync

import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson


sealed class DataSyncJson {
data class StringValue(val value: String) : DataSyncJson()
data class ObjectValue(val value: Map<String, String>) : DataSyncJson()

// Convenience getters
fun asString(): String? = (this as? StringValue)?.value

fun asInt(): Int? = asString()?.toIntOrNull()

fun asBool(): Boolean? = asString()?.let {
when (it.lowercase()) {
"true", "1" -> true
"false", "0" -> false
else -> null
}
}

fun asMap(): Map<String, String>? = (this as? ObjectValue)?.value

fun asTimezone(): TimezoneSetting? = (this as? ObjectValue)?.toTimezoneSetting()

fun asFeedStatus(): FeedStatus? = (this as? ObjectValue)?.toFeedStatus()

fun asFeedItemStatus(): FeedItemStatus? = (this as? ObjectValue)?.toFeedItemStatus()

// Type-specific converters
private fun ObjectValue.toTimezoneSetting(): TimezoneSetting? {
val enabled = value["timezone_enabled"] ?: return null
val identifier = value["timezone_identifier"] ?: return null
return TimezoneSetting(
timezoneEnabled = enabled.lowercase() == "true",
timezoneIdentifier = identifier
)
}

private fun ObjectValue.toFeedStatus(): FeedStatus? {
return FeedStatus(
chatPubkey = value["chat_pubkey"] ?: "",
feedUrl = value["feed_url"] ?: return null,
feedId = value["feed_id"] ?: return null,
subscribed = value["subscribed"]?.lowercase() == "true",
satsPerMinute = value["sats_per_minute"]?.toIntOrNull() ?: 0,
playerSpeed = value["player_speed"]?.toDoubleOrNull() ?: 1.0,
itemId = value["item_id"] ?: ""
)
}

private fun ObjectValue.toFeedItemStatus(): FeedItemStatus? {
return FeedItemStatus(
duration = value["duration"]?.toIntOrNull() ?: return null,
currentTime = value["current_time"]?.toIntOrNull() ?: return null
)
}

// Convert raw value to DataSyncJson
companion object {
fun fromAny(value: Any): DataSyncJson {
return when (value) {
is String -> StringValue(value)
is Map<*, *> -> {
@Suppress("UNCHECKED_CAST")
ObjectValue(value as Map<String, String>)
}
else -> StringValue(value.toString())
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package chat.sphinx.wrapper_common.datasync

inline val DataSyncKey.isTipAmount: Boolean
get() = this is DataSyncKey.TipAmount

inline val DataSyncKey.isPrivatePhoto: Boolean
get() = this is DataSyncKey.PrivatePhoto

inline val DataSyncKey.isTimezone: Boolean
get() = this is DataSyncKey.Timezone

inline val DataSyncKey.isFeedStatus: Boolean
get() = this is DataSyncKey.FeedStatus

inline val DataSyncKey.isFeedItemStatus: Boolean
get() = this is DataSyncKey.FeedItemStatus

inline val DataSyncKey.isUnknown: Boolean
get() = this is DataSyncKey.Unknown

@Suppress("NOTHING_TO_INLINE")
inline fun String.toDataSyncKey(): DataSyncKey =
when (this) {
DataSyncKey.TIP_AMOUNT -> DataSyncKey.TipAmount
DataSyncKey.PRIVATE_PHOTO -> DataSyncKey.PrivatePhoto
DataSyncKey.TIMEZONE -> DataSyncKey.Timezone
DataSyncKey.FEED_STATUS -> DataSyncKey.FeedStatus
DataSyncKey.FEED_ITEM_STATUS -> DataSyncKey.FeedItemStatus
else -> DataSyncKey.Unknown(this)
}

sealed class DataSyncKey {

companion object {
const val TIP_AMOUNT = "tip_amount"
const val PRIVATE_PHOTO = "private_photo"
const val TIMEZONE = "timezone"
const val FEED_STATUS = "feed_status"
const val FEED_ITEM_STATUS = "feed_item_status"
}

abstract val value: String

object TipAmount : DataSyncKey() {
override val value: String
get() = TIP_AMOUNT
}

object PrivatePhoto : DataSyncKey() {
override val value: String
get() = PRIVATE_PHOTO
}

object Timezone : DataSyncKey() {
override val value: String
get() = TIMEZONE
}

object FeedStatus : DataSyncKey() {
override val value: String
get() = FEED_STATUS
}

object FeedItemStatus : DataSyncKey() {
override val value: String
get() = FEED_ITEM_STATUS
}

data class Unknown(override val value: String) : DataSyncKey()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package chat.sphinx.wrapper_common.datasync


@JvmInline
value class DataSyncValue(val value: String) {
init {
require(value.isNotEmpty()) {
"DataSyncValue cannot be empty"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package chat.sphinx.wrapper_common.datasync

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi

@JsonClass(generateAdapter = true)
data class FeedItemStatus(
@Json(name = "duration") val duration: Int,
@Json(name = "current_time") val currentTime: Int
) {
val progressPercentage: Double
get() = if (duration > 0) (currentTime.toDouble() / duration) * 100 else 0.0

val remainingTime: Int
get() = maxOf(0, duration - currentTime)

val isCompleted: Boolean
get() = currentTime >= duration

@Throws(AssertionError::class)
fun toJson(moshi: Moshi): String {
val adapter = moshi.adapter(FeedItemStatus::class.java)
return adapter.toJson(this)
}

companion object {
fun String.toFeedItemStatusNull(moshi: Moshi): FeedItemStatus? {
return try {
this.toFeedItemStatus(moshi)
} catch (e: Exception) {
null
}
}

@Throws(JsonDataException::class)
fun String.toFeedItemStatus(moshi: Moshi): FeedItemStatus {
val adapter = moshi.adapter(FeedItemStatus::class.java)
return adapter.fromJson(this) ?: throw IllegalArgumentException("Invalid JSON for FeedItemStatus")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package chat.sphinx.wrapper_common.datasync

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi

@JsonClass(generateAdapter = true)
data class FeedStatus(
@Json(name = "chat_pubkey") val chatPubkey: String,
@Json(name = "feed_url") val feedUrl: String,
@Json(name = "feed_id") val feedId: String,
@Json(name = "subscribed") val subscribed: Boolean,
@Json(name = "sats_per_minute") val satsPerMinute: Int,
@Json(name = "player_speed") val playerSpeed: Double,
@Json(name = "item_id") val itemId: String
) {
@Throws(AssertionError::class)
fun toJson(moshi: Moshi): String {
val adapter = moshi.adapter(FeedStatus::class.java)
return adapter.toJson(this)
}

companion object {
fun String.toFeedStatusNull(moshi: Moshi): FeedStatus? {
return try {
this.toFeedStatus(moshi)
} catch (e: Exception) {
null
}
}

@Throws(JsonDataException::class)
fun String.toFeedStatus(moshi: Moshi): FeedStatus {
val adapter = moshi.adapter(FeedStatus::class.java)
return adapter.fromJson(this) ?: throw IllegalArgumentException("Invalid JSON for FeedStatus")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package chat.sphinx.wrapper_common.datasync

import chat.sphinx.wrapper_common.toDateTime
import com.squareup.moshi.*

@JsonClass(generateAdapter = true)
data class ItemsResponse(
val items: List<SettingItem>
) {
@Throws(AssertionError::class)
fun toJson(moshi: Moshi): String {
val adapter = moshi.adapter(ItemsResponse::class.java)
return adapter.toJson(this)
}

fun toOriginalFormatJson(moshi: Moshi): String? {
return try {
toJson(moshi)
} catch (e: Exception) {
null
}
}

fun getItemIndex(key: String, identifier: String): Int =
items.indexOfFirst { it.key == key && it.identifier == identifier }

companion object {
fun String.toItemsResponseNull(moshi: Moshi): ItemsResponse? {
return try {
this.toItemsResponse(moshi)
} catch (e: Exception) {
null
}
}

@Throws(JsonDataException::class)
fun String.toItemsResponse(moshi: Moshi): ItemsResponse {
val adapter = moshi.adapter(ItemsResponse::class.java)
return adapter.fromJson(this) ?: throw IllegalArgumentException("Invalid JSON for ItemsResponse")
}
}
}
Loading