From 6467b23c653880d5845395d8b34fb09564c61f3c Mon Sep 17 00:00:00 2001 From: kitsunyan Date: Wed, 29 Jul 2020 23:46:43 +0300 Subject: [PATCH] Search in description, rank search results --- .../kitsunyan/foxydroid/database/Database.kt | 42 ++++++++++++++----- .../nya/kitsunyan/foxydroid/entity/Product.kt | 7 +--- .../kitsunyan/foxydroid/entity/ProductItem.kt | 7 ++-- .../kitsunyan/foxydroid/index/IndexMerger.kt | 12 +++--- .../foxydroid/screen/ProductAdapter.kt | 5 +-- .../foxydroid/screen/ProductsAdapter.kt | 15 +++++++ .../foxydroid/screen/ProductsFragment.kt | 7 ++-- .../foxydroid/widget/DividerItemDecoration.kt | 6 --- 8 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt index 6be0329..f044ea5 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt @@ -75,6 +75,7 @@ object Database { const val ROW_PACKAGE_NAME = "package_name" const val ROW_NAME = "name" const val ROW_SUMMARY = "summary" + const val ROW_DESCRIPTION = "description" const val ROW_ADDED = "added" const val ROW_UPDATED = "updated" const val ROW_VERSION_CODE = "version_code" @@ -90,6 +91,7 @@ object Database { $ROW_PACKAGE_NAME TEXT NOT NULL, $ROW_NAME TEXT NOT NULL, $ROW_SUMMARY TEXT NOT NULL, + $ROW_DESCRIPTION TEXT NOT NULL, $ROW_ADDED INTEGER NOT NULL, $ROW_UPDATED INTEGER NOT NULL, $ROW_VERSION_CODE INTEGER NOT NULL, @@ -148,6 +150,7 @@ object Database { object Synthetic { const val ROW_CAN_UPDATE = "can_update" + const val ROW_MATCH_RANK = "match_rank" } } @@ -191,7 +194,7 @@ object Database { db.execSQL(it.formatCreateTable(it.name)) !it.memory } - if (shouldVacuum.any { it }) { + if (shouldVacuum.any { it } && !db.inTransaction()) { db.execSQL("VACUUM") } true @@ -215,7 +218,7 @@ object Database { !it.memory } } - if (shouldVacuum.any { it }) { + if (shouldVacuum.any { it } && !db.inTransaction()) { db.execSQL("VACUUM") } } @@ -230,7 +233,9 @@ object Database { for (table in tables) { db.execSQL("DROP TABLE IF EXISTS $table") } - db.execSQL("VACUUM") + if (!db.inTransaction()) { + db.execSQL("VACUUM") + } } } @@ -378,7 +383,7 @@ object Database { object ProductAdapter { fun get(packageName: String, signal: CancellationSignal?): List { return db.query(Schema.Product.name, - columns = arrayOf(Schema.Product.ROW_REPOSITORY_ID, Schema.Product.ROW_DATA), + columns = arrayOf(Schema.Product.ROW_REPOSITORY_ID, Schema.Product.ROW_DESCRIPTION, Schema.Product.ROW_DATA), selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), signal = signal).use { it.asSequence().map(::transform).toList() } } @@ -404,7 +409,19 @@ object Database { product.${Schema.Product.ROW_COMPATIBLE} != 0 AND product.${Schema.Product.ROW_VERSION_CODE} > COALESCE(installed.${Schema.Installed.ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches) AS ${Schema.Synthetic.ROW_CAN_UPDATE}, product.${Schema.Product.ROW_COMPATIBLE}, - product.${Schema.Product.ROW_DATA_ITEM}, MAX((product.${Schema.Product.ROW_COMPATIBLE} AND + product.${Schema.Product.ROW_DATA_ITEM},""" + + if (searchQuery.isNotEmpty()) { + builder += """(((product.${Schema.Product.ROW_NAME} LIKE ? OR + product.${Schema.Product.ROW_SUMMARY} LIKE ?) * 7) | + ((product.${Schema.Product.ROW_PACKAGE_NAME} LIKE ?) * 3) | + (product.${Schema.Product.ROW_DESCRIPTION} LIKE ?)) AS ${Schema.Synthetic.ROW_MATCH_RANK},""" + builder %= List(4) { "%$searchQuery%" } + } else { + builder += "0 AS ${Schema.Synthetic.ROW_MATCH_RANK}," + } + + builder += """MAX((product.${Schema.Product.ROW_COMPATIBLE} AND (installed.${Schema.Installed.ROW_SIGNATURE} IS NULL OR $signatureMatches)) || PRINTF('%016X', product.${Schema.Product.ROW_VERSION_CODE})) FROM ${Schema.Product.name} AS product""" @@ -429,10 +446,7 @@ object Database { builder %= category } if (searchQuery.isNotEmpty()) { - builder += """AND (product.${Schema.Product.ROW_PACKAGE_NAME} LIKE ? OR - product.${Schema.Product.ROW_NAME} LIKE ? OR - product.${Schema.Product.ROW_SUMMARY} LIKE ?)""" - builder %= List(3) { "%$searchQuery%" } + builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0""" } builder += "GROUP BY product.${Schema.Product.ROW_PACKAGE_NAME} HAVING 1" @@ -440,6 +454,9 @@ object Database { builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}" } builder += "ORDER BY" + if (searchQuery.isNotEmpty()) { + builder += """${Schema.Synthetic.ROW_MATCH_RANK} DESC,""" + } when (order) { ProductItem.Order.NAME -> Unit ProductItem.Order.DATE_ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC," @@ -452,7 +469,8 @@ object Database { private fun transform(cursor: Cursor): Product { return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA)) - .jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)), it) } + .jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)), + cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)), it) } } fun transformItem(cursor: Cursor): ProductItem { @@ -463,7 +481,8 @@ object Database { cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY)), cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)).orEmpty(), cursor.getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0, - cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0, it) } + cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0, + cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)), it) } } } @@ -585,6 +604,7 @@ object Database { put(Schema.Product.ROW_PACKAGE_NAME, product.packageName) put(Schema.Product.ROW_NAME, product.name) put(Schema.Product.ROW_SUMMARY, product.summary) + put(Schema.Product.ROW_DESCRIPTION, product.description) put(Schema.Product.ROW_ADDED, product.added) put(Schema.Product.ROW_UPDATED, product.updated) put(Schema.Product.ROW_VERSION_CODE, product.versionCode) diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/Product.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/Product.kt index 066744d..7d511ee 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/Product.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/Product.kt @@ -54,7 +54,7 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St get() = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct().toList() fun item(): ProductItem { - return ProductItem(repositoryId, packageName, name, summary, icon, version, "", compatible, false) + return ProductItem(repositoryId, packageName, name, summary, icon, version, "", compatible, false, 0) } fun canUpdate(installedItem: InstalledItem?): Boolean { @@ -67,7 +67,6 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St generator.writeStringField("packageName", packageName) generator.writeStringField("name", name) generator.writeStringField("summary", summary) - generator.writeStringField("description", description) generator.writeStringField("whatsNew", whatsNew) generator.writeStringField("icon", icon) generator.writeStringField("authorName", author.name) @@ -133,11 +132,10 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St (installedItem == null || installedItem.signature in extract(it).signatures) }, { extract(it).versionCode })) } - fun deserialize(repositoryId: Long, parser: JsonParser): Product { + fun deserialize(repositoryId: Long, description: String, parser: JsonParser): Product { var packageName = "" var name = "" var summary = "" - var description = "" var whatsNew = "" var icon = "" var authorName = "" @@ -161,7 +159,6 @@ data class Product(val repositoryId: Long, val packageName: String, val name: St it.string("packageName") -> packageName = valueAsString it.string("name") -> name = valueAsString it.string("summary") -> summary = valueAsString - it.string("description") -> description = valueAsString it.string("whatsNew") -> whatsNew = valueAsString it.string("icon") -> icon = valueAsString it.string("authorName") -> authorName = valueAsString diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt index 1cad3f5..a188910 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt @@ -7,7 +7,7 @@ import nya.kitsunyan.foxydroid.utility.extension.json.* data class ProductItem(val repositoryId: Long, val packageName: String, val name: String, val summary: String, val icon: String, val version: String, val installedVersion: String, - val compatible: Boolean, val canUpdate: Boolean) { + val compatible: Boolean, val canUpdate: Boolean, val matchRank: Int) { enum class Order(val titleResId: Int) { NAME(R.string.name), DATE_ADDED(R.string.date_added), @@ -22,7 +22,8 @@ data class ProductItem(val repositoryId: Long, val packageName: String, companion object { fun deserialize(repositoryId: Long, packageName: String, name: String, summary: String, - installedVersion: String, compatible: Boolean, canUpdate: Boolean, parser: JsonParser): ProductItem { + installedVersion: String, compatible: Boolean, canUpdate: Boolean, matchRank: Int, + parser: JsonParser): ProductItem { var icon = "" var version = "" parser.forEachKey { @@ -33,7 +34,7 @@ data class ProductItem(val repositoryId: Long, val packageName: String, } } return ProductItem(repositoryId, packageName, name, summary, icon, - version, installedVersion, compatible, canUpdate) + version, installedVersion, compatible, canUpdate, matchRank) } } } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/index/IndexMerger.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/index/IndexMerger.kt index f5e7bee..33ca74f 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/index/IndexMerger.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/index/IndexMerger.kt @@ -17,7 +17,7 @@ class IndexMerger(file: File): Closeable { init { db.execWithResult("PRAGMA synchronous = OFF") db.execWithResult("PRAGMA journal_mode = OFF") - db.execSQL("CREATE TABLE product (package_name TEXT PRIMARY KEY, data BLOB NOT NULL)") + db.execSQL("CREATE TABLE product (package_name TEXT PRIMARY KEY, description TEXT NOT NULL, data BLOB NOT NULL)") db.execSQL("CREATE TABLE releases (package_name TEXT PRIMARY KEY, data BLOB NOT NULL)") db.beginTransaction() } @@ -28,6 +28,7 @@ class IndexMerger(file: File): Closeable { Json.factory.createGenerator(outputStream).use { it.writeDictionary(product::serialize) } db.insert("product", null, ContentValues().apply { put("package_name", product.packageName) + put("description", product.description) put("data", outputStream.toByteArray()) }) } @@ -60,14 +61,15 @@ class IndexMerger(file: File): Closeable { fun forEach(repositoryId: Long, windowSize: Int, callback: (List, Int) -> Unit) { closeTransaction() - db.rawQuery("""SELECT product.data AS p, releases.data AS d FROM product + db.rawQuery("""SELECT product.description, product.data AS pd, releases.data AS rd FROM product LEFT JOIN releases ON product.package_name = releases.package_name""", null) ?.use { it.asSequence().map { - val product = Json.factory.createParser(it.getBlob(0)).use { + val description = it.getString(0) + val product = Json.factory.createParser(it.getBlob(1)).use { it.nextToken() - Product.deserialize(repositoryId, it) + Product.deserialize(repositoryId, description, it) } - val releases = it.getBlob(1)?.let { Json.factory.createParser(it).use { + val releases = it.getBlob(2)?.let { Json.factory.createParser(it).use { it.nextToken() it.collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize) } }.orEmpty() diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductAdapter.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductAdapter.kt index 07ba1b1..0411195 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductAdapter.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductAdapter.kt @@ -14,7 +14,6 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.net.Uri import android.os.Parcel -import android.text.Html import android.text.SpannableStringBuilder import android.text.format.DateFormat import android.text.style.BulletSpan @@ -37,6 +36,7 @@ import android.widget.Switch import android.widget.TextView import android.widget.Toast import androidx.core.graphics.ColorUtils +import androidx.core.text.HtmlCompat import androidx.core.text.util.LinkifyCompat import androidx.recyclerview.widget.RecyclerView import nya.kitsunyan.foxydroid.R @@ -1207,8 +1207,7 @@ class ProductAdapter(private val callbacks: Callbacks, private val columns: Int) } private fun formatHtml(text: String): SpannableStringBuilder { - val html = if (Android.sdk(24)) Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY) else - @Suppress("DEPRECATION") Html.fromHtml(text) + val html = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY) val builder = run { val builder = SpannableStringBuilder(html) val last = builder.indexOfLast { it != '\n' } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsAdapter.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsAdapter.kt index 6f580b7..e82fa6d 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsAdapter.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsAdapter.kt @@ -20,6 +20,7 @@ import nya.kitsunyan.foxydroid.utility.Utils import nya.kitsunyan.foxydroid.utility.extension.resources.* import nya.kitsunyan.foxydroid.utility.extension.text.* import nya.kitsunyan.foxydroid.widget.CursorRecyclerAdapter +import nya.kitsunyan.foxydroid.widget.DividerItemDecoration class ProductsAdapter(private val onClick: (ProductItem) -> Unit): CursorRecyclerAdapter() { @@ -68,6 +69,20 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit): } } + fun configureDivider(context: Context, position: Int, configuration: DividerItemDecoration.Configuration) { + val currentItem = if (getItemEnumViewType(position) == ViewType.PRODUCT) getProductItem(position) else null + val nextItem = if (position + 1 < itemCount && getItemEnumViewType(position + 1) == ViewType.PRODUCT) + getProductItem(position + 1) else null + when { + currentItem != null && nextItem != null && currentItem.matchRank != nextItem.matchRank -> { + configuration.set(true, false, 0, 0) + } + else -> { + configuration.set(true, false, context.resources.sizeScaled(72), 0) + } + } + } + var repositories: Map = emptyMap() set(value) { field = value diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt index b339b02..e81f783 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt @@ -17,7 +17,6 @@ import nya.kitsunyan.foxydroid.database.CursorOwner import nya.kitsunyan.foxydroid.database.Database import nya.kitsunyan.foxydroid.entity.ProductItem import nya.kitsunyan.foxydroid.utility.RxUtils -import nya.kitsunyan.foxydroid.utility.extension.resources.* import nya.kitsunyan.foxydroid.widget.DividerItemDecoration import nya.kitsunyan.foxydroid.widget.RecyclerFastScroller @@ -79,9 +78,9 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { isVerticalScrollBarEnabled = false setHasFixedSize(true) recycledViewPool.setMaxRecycledViews(ProductsAdapter.ViewType.PRODUCT.ordinal, 30) - adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) } - addItemDecoration(DividerItemDecoration(context, - DividerItemDecoration.fixed(context.resources.sizeScaled(72), 0))) + val adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) } + this.adapter = adapter + addItemDecoration(DividerItemDecoration(context, adapter::configureDivider)) RecyclerFastScroller(this) recyclerView = this } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/widget/DividerItemDecoration.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/widget/DividerItemDecoration.kt index d5165e2..2d58a86 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/widget/DividerItemDecoration.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/widget/DividerItemDecoration.kt @@ -11,12 +11,6 @@ import kotlin.math.* class DividerItemDecoration(context: Context, private val configure: (context: Context, position: Int, configuration: Configuration) -> Unit): RecyclerView.ItemDecoration() { - companion object { - fun fixed(start: Int, end: Int): (Context, Int, Configuration) -> Unit = { _, _, configuration -> - configuration.set(true, false, start, end) - } - } - interface Configuration { fun set(needDivider: Boolean, toTop: Boolean, paddingStart: Int, paddingEnd: Int) }