Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ interface IndexDao {

allCategoryAppRelations += metadata.categories.map { CategoryAppRelation(appId, it) }

allAppNames += metadata.name.localizedAppName(appId)
allAppNames += metadata.name?.localizedAppName(appId)
?: listOf(LocalizedAppNameEntity(appId, locale = "en-US", name = packageName))
metadata.summary?.localizedAppSummary(appId)?.let { allAppSummaries += it }
metadata.description?.localizedAppDescription(appId)?.let { allAppDescriptions += it }
metadata.icon?.localizedAppIcon(appId)?.let { allAppIcons += it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ fun MetadataV2.donateEntity(appId: Int): List<DonateEntity>? {
if (openCollective != null) {
add(DonateEntity(OPEN_COLLECTIVE_ID, openCollective, appId))
}
if (flattrID != null) {
add(DonateEntity(FLATTR_ID, flattrID, appId))
}
if (!donate.isNullOrEmpty()) {
add(DonateEntity(REGULAR, donate.joinToString(STRING_LIST_SEPARATOR), appId))
}
Expand All @@ -60,12 +57,12 @@ fun List<DonateEntity>.toDonation(): Donation {
var regular: List<String>? = null
for (entity in this) {
when (entity.type) {
BITCOIN_ADD -> bitcoinAddress = entity.value
FLATTR_ID -> flattrId = entity.value
LIBERAPAY_ID -> liberapayId = entity.value
LITECOIN_ADD -> litecoinAddress = entity.value
BITCOIN_ADD -> bitcoinAddress = entity.value
FLATTR_ID -> flattrId = entity.value
LIBERAPAY_ID -> liberapayId = entity.value
LITECOIN_ADD -> litecoinAddress = entity.value
OPEN_COLLECTIVE_ID -> openCollectiveId = entity.value
REGULAR -> regular = entity.value.split(STRING_LIST_SEPARATOR)
REGULAR -> regular = entity.value.split(STRING_LIST_SEPARATOR)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,19 @@ internal fun IndexV1.toV2(): IndexV2 {
versions = versions?.associate { packageV1 ->
packageV1.hash to packageV1.toVersionV2(
whatsNew = whatsNew,
packageAntiFeatures = app.antiFeatures + (packageV1.antiFeatures ?: emptyList())
packageAntiFeatures = app.antiFeatures + (packageV1.antiFeatures
?: emptyList()),
)
} ?: emptyMap(),
metadata = app.toV2(preferredSigner)
metadata = app.toV2(preferredSigner),
)
packagesV2.putIfAbsent(app.packageName, packageV2)
}

return IndexV2(
repo = repo.toRepoV2(
categories = categories,
antiFeatures = antiFeatures
antiFeatures = antiFeatures,
),
packages = packagesV2,
)
Expand Down Expand Up @@ -108,7 +109,6 @@ private fun AppV1.toV2(preferredSigner: String?): MetadataV2 = MetadataV2(
changelog = changelog,
donate = if (donate != null) listOf(donate) else emptyList(),
featureGraphic = localized?.localizedIcon(packageName) { it.featureGraphic },
flattrID = flattrID,
issueTracker = issueTracker,
liberapay = liberapay,
license = license,
Expand Down Expand Up @@ -176,7 +176,7 @@ private fun PackageV1.toVersionV2(
usesPermission = usesPermission.map { PermissionV2(it.name, it.maxSdk) },
usesPermissionSdk23 = usesPermission23.map { PermissionV2(it.name, it.maxSdk) },
features = features?.map { FeatureV2(it) } ?: emptyList(),
nativecode = nativeCode ?: emptyList()
nativecode = nativeCode ?: emptyList(),
),
)

Expand Down Expand Up @@ -240,7 +240,7 @@ private inline fun Map<String, Localized>.localizedScreenshots(
}

private inline fun <K, V, M> Map<K, V>.mapValuesNotNull(
block: (Map.Entry<K, V>) -> M?
block: (Map.Entry<K, V>) -> M?,
): Map<K, M> {
val map = HashMap<K, M>()
forEach { entry ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ data class AppV1(
val categories: List<String> = emptyList(),
val changelog: String? = null,
val donate: String? = null,
val flattrID: String? = null,
val issueTracker: String? = null,
val lastUpdated: Long? = null,
val liberapay: String? = null,
Expand Down
37 changes: 21 additions & 16 deletions app/src/main/kotlin/com/looker/droidify/sync/v2/EntrySyncable.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.looker.droidify.sync.v2

import android.content.Context
import android.util.Log
import com.looker.droidify.data.model.Repo
import com.looker.droidify.network.Downloader
import com.looker.droidify.network.percentBy
Expand All @@ -14,14 +15,11 @@ import com.looker.droidify.sync.common.downloadIndex
import com.looker.droidify.sync.toJarScope
import com.looker.droidify.sync.v2.model.Entry
import com.looker.droidify.sync.v2.model.IndexV2
import com.looker.droidify.sync.v2.model.IndexV2Diff
import com.looker.droidify.sync.v2.model.IndexV2Merger
import com.looker.droidify.utility.common.cache.Cache
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToStream

class EntrySyncable(
private val context: Context,
Expand All @@ -48,8 +46,8 @@ class EntrySyncable(
block(
SyncState.IndexDownload.Failure(
repo.id,
IllegalStateException("Empty entry v2 jar")
)
IllegalStateException("Empty entry v2 jar"),
),
)
return@withContext
} else {
Expand Down Expand Up @@ -93,17 +91,24 @@ class EntrySyncable(
block(SyncState.IndexDownload.Progress(repo.id, percent))
},
)
val diff = async {
JsonParser.decodeFromString<IndexV2Diff>(diffFile.readBytes().decodeToString())
}
val oldIndex = async {
JsonParser.decodeFromString<IndexV2>(indexFile.readBytes().decodeToString())
}
try {
diff.await().patchInto(oldIndex.await()) { index ->
diffFile.delete()
Json.encodeToStream(index, indexFile.outputStream())
}
indexFile
.takeIf { it.exists() && it.length() > 0 }
?.let { indexFile ->
IndexV2Merger(indexFile).use { merger ->
merger.processDiff(
diffFile.inputStream(),
).let {
Log.d(
"EntrySyncable",
"merged diff file $diffFile, success = $it, indexFile = $indexFile.",
)
}
}
JsonParser.decodeFromString<IndexV2>(
indexFile.readBytes().decodeToString(),
)
}
} catch (t: Throwable) {
block(SyncState.JsonParsing.Failure(repo.id, t))
return@withContext
Expand Down
27 changes: 1 addition & 26 deletions app/src/main/kotlin/com/looker/droidify/sync/v2/model/IndexV2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class IndexV2(
val repo: RepoV2,
val packages: Map<String, PackageV2>
val packages: Map<String, PackageV2>,
)

@Serializable
data class IndexV2Diff(
val repo: RepoV2Diff,
val packages: Map<String, PackageV2Diff?>
) {
fun patchInto(index: IndexV2, saveIndex: (IndexV2) -> Unit): IndexV2 {
val packagesToRemove = packages.filter { it.value == null }.keys
val packagesToAdd = packages
.mapNotNull { (key, value) ->
value?.let { value ->
if (index.packages.keys.contains(key))
index.packages[key]?.let { value.patchInto(it) }
else value.toPackage()
}?.let { key to it }
}

val newIndex = index.copy(
repo = repo.patchInto(index.repo),
packages = index.packages.minus(packagesToRemove).plus(packagesToAdd),
)
saveIndex(newIndex)
return newIndex
}
}
151 changes: 151 additions & 0 deletions app/src/main/kotlin/com/looker/droidify/sync/v2/model/MergerV2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.looker.droidify.sync.v2.model

import android.util.Log
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.longOrNull
import java.io.File
import java.io.InputStream

/**
* Merger for applying JSON Merge Patch (RFC 7386) to IndexV2 instances.
* Adapted from Neo Store.
*/
class IndexV2Merger(private val baseFile: File) : AutoCloseable {
private val json = Json {
ignoreUnknownKeys = true
explicitNulls = true
}

@OptIn(ExperimentalSerializationApi::class)
fun getCurrentIndex(): IndexV2? = json.decodeFromStream(baseFile.inputStream())

fun processDiff(
diffStream: InputStream,
): Boolean {
val tempFile = File.createTempFile("merged_", ".json")

try {
val hasChanged = merge(baseFile, diffStream, tempFile)

if (hasChanged) {
// Save the merged result
tempFile.copyTo(baseFile, overwrite = true)
tempFile.inputStream().use { inputStream ->
val mergedElement = json.decodeFromStream<JsonElement>(inputStream)
val timestamp = getTimestamp(mergedElement)
baseFile.setLastModified(timestamp)
}
}

Log.d("IndexV2Merger", "Merged a diff JSON into the base: $hasChanged")
return hasChanged
} finally {
tempFile.delete()
}
}

private fun merge(baseFile: File, diffStream: InputStream, outputFile: File): Boolean {
val baseElement =
runCatching { baseFile.inputStream().use { json.decodeFromStream<JsonElement>(it) } }
.fold(
onSuccess = { it },
onFailure = {
throw Exception(it.message.orEmpty(), it)
},
)
val diffElement = runCatching { json.decodeFromStream<JsonElement>(diffStream) }
.fold(
onSuccess = { it },
onFailure = {
throw Exception(it.message.orEmpty(), it)
},
)

// No need to apply a diff older or same as base
val baseTimestamp = getTimestamp(baseElement)
val diffTimestamp = getTimestamp(diffElement)

if (diffTimestamp <= baseTimestamp) {
baseFile.copyTo(outputFile, overwrite = true)
return false
}

// Apply the merge patch
val mergedElement = mergePatch(baseElement, diffElement)

// Ensure the timestamp is updated
val mergedObj = mergedElement.jsonObject.toMutableMap()
val repoObj = (mergedObj["repo"] as? JsonObject)?.toMutableMap() ?: run {
baseFile.copyTo(outputFile, overwrite = true)
return false
}

repoObj["timestamp"] = JsonPrimitive(diffTimestamp)
val finalResult = JsonObject(mergedObj + ("repo" to JsonObject(repoObj)))

outputFile.outputStream().use { outputStream ->
json.encodeToStream(finalResult, outputStream)
}

return baseElement != finalResult
}

/**
* Applies a JSON Merge Patch (RFC 7386) to the target JSON element. RFC 7386 rules:
* - If patch is not an object, replace target entirely
* - If patch value is null, remove the key from target
* - If patch value is an object, recursively merge with target
* - Otherwise, replace target value with patch value
*/
private fun mergePatch(target: JsonElement, patch: JsonElement): JsonElement {
if (patch !is JsonObject || target !is JsonObject) return patch
val result = target.jsonObject.toMutableMap()

for ((key, value) in patch) {
// No change when object is empty
if (value is JsonObject && value.jsonObject.isEmpty()) continue

when (value) {
// Remove null objects
is JsonNull -> {
result.remove(key)
}

// Recursively merge objects
is JsonObject -> {
val targetValue = target.jsonObject[key]
result[key] = if (targetValue is JsonObject) {
mergePatch(targetValue, value)
} else {
// If target doesn't have this key or it's not an object, use the patch value
value
}
}

// Replace primitive values entirely
else -> {
result[key] = value
}
}
}

return JsonObject(result)
}

private fun getTimestamp(element: JsonElement): Long {
return (element.jsonObject["repo"]?.jsonObject?.get("timestamp") as? JsonPrimitive)?.longOrNull
?: 0L
}

override fun close() {
// Cleanup when needed
}
}
Loading
Loading