diff --git a/libs/database/src/main/java/org/fdroid/database/AppDao.kt b/libs/database/src/main/java/org/fdroid/database/AppDao.kt index 2cc79e034..1ac9eb469 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -141,6 +141,8 @@ public interface AppDao { public fun getInstalledAppListItems(packageManager: PackageManager): LiveData> + public suspend fun getAppSearchItems(searchQuery: String): List + public fun getNumberOfAppsInCategory(category: String): Int public fun getNumberOfAppsInRepository(repoId: Long): Int @@ -706,6 +708,20 @@ internal interface AppDaoInt : AppDao { } } + @Transaction + @Query( + """ + SELECT repoId, packageName, app.lastUpdated, app.name, app.summary, + app.description, app.authorName, app.categories, + matchinfo(${AppMetadataFts.TABLE}, 'pcx') + FROM ${AppMetadata.TABLE} AS app + JOIN PreferredRepo USING (packageName) + JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName) + WHERE ${AppMetadataFts.TABLE} MATCH :searchQuery AND + repoId = preferredRepoId""" + ) + override suspend fun getAppSearchItems(searchQuery: String): List + // // Misc Queries // diff --git a/libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt b/libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt new file mode 100644 index 000000000..1c3c81a46 --- /dev/null +++ b/libs/database/src/main/java/org/fdroid/database/AppSearchItem.kt @@ -0,0 +1,98 @@ +package org.fdroid.database + +import androidx.core.os.LocaleListCompat +import androidx.room.ColumnInfo +import androidx.room.Ignore +import androidx.room.Relation +import org.fdroid.LocaleChooser.getBestLocale +import org.fdroid.index.v2.FileV2 +import org.fdroid.index.v2.LocalizedTextV2 +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.min + +@OptIn(ExperimentalUnsignedTypes::class) +@ConsistentCopyVisibility +public data class AppSearchItem internal constructor( + public val repoId: Long, + public val packageName: String, + public val lastUpdated: Long, + public val name: LocalizedTextV2? = null, + public val summary: LocalizedTextV2? = null, + public val description: LocalizedTextV2? = null, + public val authorName: String? = null, + public val categories: List? = null, + @Relation( + parentColumn = "packageName", + entityColumn = "packageName", + ) + internal val localizedIcon: List? = null, + @Suppress("ArrayInDataClass") + @ColumnInfo("matchinfo(${AppMetadataFts.TABLE}, 'pcx')") + internal val matchInfo: ByteArray, +) : Comparable { + public fun getIcon(localeList: LocaleListCompat): FileV2? { + return localizedIcon?.filter { icon -> + icon.repoId == repoId + }?.toLocalizedFileV2().getBestLocale(localeList) + } + + @Ignore + public val score: Double + + init { + val info = matchInfo.toIntArray() + val numPhrases = info[0] + val numColumns = info[1] + val scoreMap = mutableMapOf() + for (phrase in 0 until numPhrases) { + val offset = 2 + phrase * numColumns * 3 + // start with 1 below, because we don't care about repoId column + for (column in 1 until numColumns) { + val numHitsInRow = info[offset + 3 * column] + // increase score if this column had a hit + if (numHitsInRow > 0) { + // each hit in a column only contributes to the score once + scoreMap.getOrPut(column) { + weights[column] ?: error("No weight for column $column") + } + } + } + } + val weeksOld = (System.currentTimeMillis() - lastUpdated) / (1000 * 60 * 60 * 24 * 7) + val punishment = min(100, weeksOld / 3) + score = scoreMap.values.sum().toDouble() - punishment + } + + private fun ByteArray.toIntArray(skipSize: Int = 4): IntArray { + val intArray = IntArray(size / skipSize) + // go through each 4 bytes to turn them into integers + (indices step skipSize).forEachIndexed { intIndex, byteIndex -> + // we are cutting the first two bytes off, because we don't want to deal with UInt + // and expected integers are small enough + intArray[intIndex] = ByteBuffer.wrap(this, byteIndex, 4) + .order(ByteOrder.LITTLE_ENDIAN) + .int + } + return intArray + } + + override fun compareTo(other: AppSearchItem): Int { + val scoreComp = score.compareTo(other.score) + return if (scoreComp == 0) { + lastUpdated.compareTo(other.lastUpdated) + } else { + scoreComp + } + } +} + +@Suppress("ktlint:standard:no-multi-spaces") +private val weights = mapOf( + // 0 is repoId which we ignore + 1 to 100, // "name" + 2 to 50, // "summary" + 3 to 25, // "description" + 4 to 10, // "authorName" + 5 to 5, // "packageName" +)