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
8 changes: 6 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ android {
versionName = property.project.app.versionName
versionCode = property.project.app.versionCode
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

buildConfigField("String", "SUPPORTED_LAUNCHER_VERSION", "\"15.8.17\"")
}

Expand Down Expand Up @@ -44,7 +44,10 @@ android {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Use the signing configuration
signingConfig = signingConfigs.getByName("release")
}
Expand Down Expand Up @@ -83,4 +86,5 @@ dependencies {
testImplementation(junit.junit)
androidTestImplementation(androidx.test.ext.junit)
androidTestImplementation(androidx.test.espresso.espresso.core)
implementation(me.xdrop.fuzzywuzzy)
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,51 @@
package com.wizpizz.onepluspluslauncher.hook.features

import android.util.Log
import com.highcapable.yukihookapi.hook.param.PackageParam
import com.highcapable.yukihookapi.hook.factory.current
import com.highcapable.yukihookapi.hook.factory.field
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.param.PackageParam
import com.wizpizz.onepluspluslauncher.hook.features.HookUtils.PREF_USE_FUZZY_SEARCH
import com.wizpizz.onepluspluslauncher.hook.features.HookUtils.TAG
import java.util.ArrayList

/**
* Implements fuzzy search with intelligent ranking:
* - Prefix matches (highest priority)
* - Substring matches (medium priority)
* - Subsequence matches (lowest priority)
*/
import me.xdrop.fuzzywuzzy.FuzzySearch
import kotlin.math.roundToInt


object FuzzySearchHook {

private const val SEARCH_CONTAINER_CLASS = "com.android.launcher3.allapps.search.LauncherTaskbarAppsSearchContainerLayout"
private const val BASE_ADAPTER_ITEM_CLASS = "com.android.launcher3.allapps.BaseAllAppsAdapter\$AdapterItem"

private const val SEARCH_CONTAINER_CLASS =
"com.android.launcher3.allapps.search.LauncherTaskbarAppsSearchContainerLayout"
private const val BASE_ADAPTER_ITEM_CLASS =
"com.android.launcher3.allapps.BaseAllAppsAdapter\$AdapterItem"
private const val APP_INFO_CLASS = "com.android.launcher3.model.data.AppInfo"
private const val ARRAY_LIST_CLASS = "java.util.ArrayList"
// Scoring constants
private const val PREFIX_SCORE = 3
private const val SUBSTRING_SCORE = 2
private const val SUBSEQUENCE_SCORE = 1

private const val MATCH_THRESHOLD = 50
private const val PREFIX_MATCH_MULTIPLIER = 1.5
private const val SUBSTRING_MATCH_MULTIPLIER = 1.3
private const val SUBSEQUENCE_MATCH_MULTIPLIER = 1.1

data class FuzzyMatchResult(val appInfo: Any, val score: Int, val appName: String)

fun apply(packageParam: PackageParam) {
packageParam.apply {
SEARCH_CONTAINER_CLASS.toClassOrNull(appClassLoader)?.method {
name = "onSearchResult"
param(String::class.java.name, ARRAY_LIST_CLASS)
}?.hook {
before {
val query = args[0] as? String ?: return@before
}?.hook {
before {
val rawQuery = args[0] as? String ?: return@before

// Read preference - default to true for better search experience
val useFuzzySearch = prefs.getBoolean(PREF_USE_FUZZY_SEARCH, true)
if (!useFuzzySearch) return@before

if (!useFuzzySearch || query.isBlank()) {
return@before
}
// IME delimiters: strip spaces and single quotes from the query
val sanitizedQuery = sanitizeSearchQuery(rawQuery)
if (sanitizedQuery.isBlank()) return@before

try {
val sortedResults = performFuzzySearch(instance, query)
val sortedResults = performFuzzySearch(instance, sanitizedQuery)
if (sortedResults.isNotEmpty()) {
args[1] = sortedResults
}
Expand All @@ -57,131 +56,154 @@ object FuzzySearchHook {
} ?: Log.e(TAG, "[FuzzySearch] Could not find onSearchResult method")
}
}

private fun PackageParam.performFuzzySearch(containerInstance: Any, query: String): ArrayList<Any> {

private fun sanitizeSearchQuery(input: String): String {
// Remove spaces and single quote marks which are often used as IME delimiters
if (input.isEmpty()) return input
val builder = StringBuilder(input.length)
input.forEach { ch ->
if (ch != ' ' && ch != '\'') builder.append(ch)
}
return builder.toString()
}

private fun PackageParam.performFuzzySearch(
containerInstance: Any,
query: String
): ArrayList<Any> {
// Get apps list
val appsList = getAppsListFromContainer(containerInstance) ?: return ArrayList()
val allAppInfos = getAllAppInfos(appsList) ?: return ArrayList()

// Score and filter results
val scoredResults = scoreSearchResults(allAppInfos, query)

// Sort by score and convert to adapter items
return convertToAdapterItems(scoredResults)
}

private fun getAppsListFromContainer(containerInstance: Any): Any? {
return try {
val appsViewField = containerInstance.javaClass.field { name = "mAppsView"; superClass(true) }
val appsViewField =
containerInstance.javaClass.field { name = "mAppsView"; superClass(true) }
val appsViewInstance = appsViewField.get(containerInstance).any() ?: return null

appsViewInstance.current().method { name = "getAlphabeticalAppsList"; superClass() }.call()

appsViewInstance.current().method { name = "getAlphabeticalAppsList"; superClass() }
.call()
?: appsViewInstance.current().method { name = "getAppsList"; superClass() }.call()
?: appsViewInstance.current().method { name = "getApps"; superClass() }.call()
} catch (e: Throwable) {
Log.e(TAG, "[FuzzySearch] Failed to get apps list: ${e.message}")
null
}
}

private fun getAllAppInfos(appsList: Any): List<*>? {
return try {
appsList.current().method {
appsList.current().method {
name = "getApps"
superClass(true)
}.call() as? List<*>
} catch (e: Throwable) {
try {
val allAppsStore = appsList.current().method { name = "getAllAppsStore"; superClass(true) }.call()
allAppsStore?.current()?.method { name = "getApps"; superClass(true) }?.call() as? List<*>
val allAppsStore =
appsList.current().method { name = "getAllAppsStore"; superClass(true) }.call()
allAppsStore?.current()?.method { name = "getApps"; superClass(true) }
?.call() as? List<*>
} catch (e2: Throwable) {
Log.e(TAG, "[FuzzySearch] Failed to get app infos: ${e2.message}")
null
}
}
}

private fun PackageParam.scoreSearchResults(appInfos: List<*>, query: String): List<FuzzyMatchResult> {

private fun PackageParam.scoreSearchResults(
appInfos: List<*>,
query: String
): List<FuzzyMatchResult> {
val scoredResults = ArrayList<FuzzyMatchResult>()
val appInfoClass = APP_INFO_CLASS.toClass(appClassLoader)
val queryLower = query.lowercase()

appInfos.filterNotNull().forEach { appInfoObj ->
try {
if (!appInfoClass.isInstance(appInfoObj)) return@forEach

val appInfo = appInfoClass.cast(appInfoObj)
val titleField = appInfo?.javaClass?.field { name = "title"; superClass(true) }
val appName = titleField?.get(appInfo)?.any()?.toString() ?: ""
val appNameLower = appName.lowercase()

val score = calculateMatchScore(appNameLower, queryLower)
if (score > 0) {

if (score >= MATCH_THRESHOLD) {
appInfo?.let { FuzzyMatchResult(it, score, appName) }
?.let { scoredResults.add(it) }
}
} catch (e: Throwable) {
Log.e(TAG, "[FuzzySearch] Error processing app: ${e.message}")
}
}

return scoredResults
}

private fun calculateMatchScore(appNameLower: String, queryLower: String): Int {
return when {
appNameLower.startsWith(queryLower) -> PREFIX_SCORE
appNameLower.contains(queryLower) -> SUBSTRING_SCORE
isSubsequence(queryLower, appNameLower) -> SUBSEQUENCE_SCORE
else -> 0
// Base score using Weighted Ratio from FuzzyWuzzy (0..100)
val baseScore = try {
FuzzySearch.weightedRatio(appNameLower, queryLower)
} catch (t: Throwable) {
0
}

// Apply boosts based on match type
val multiplier = when {
queryLower.isEmpty() -> 1.0
appNameLower.startsWith(queryLower) -> PREFIX_MATCH_MULTIPLIER
appNameLower.contains(queryLower) -> SUBSTRING_MATCH_MULTIPLIER
isSubsequence(appNameLower, queryLower) -> SUBSEQUENCE_MATCH_MULTIPLIER
else -> 1.0
}

return (baseScore * multiplier).roundToInt()
}

private fun isSubsequence(query: String, appName: String): Boolean {
if (query.isEmpty()) return false

var queryIndex = 0
var appNameIndex = 0

while (queryIndex < query.length && appNameIndex < appName.length) {
if (query[queryIndex] == appName[appNameIndex]) {
queryIndex++

private fun isSubsequence(text: String, pattern: String): Boolean {
if (pattern.isEmpty()) return true
var textIndex = 0
var patternIndex = 0
while (textIndex < text.length && patternIndex < pattern.length) {
if (text[textIndex] == pattern[patternIndex]) {
patternIndex++
}
appNameIndex++
textIndex++
}

return queryIndex == query.length
return patternIndex == pattern.length
}

private fun PackageParam.convertToAdapterItems(scoredResults: List<FuzzyMatchResult>): ArrayList<Any> {
// Sort by score (descending), then alphabetically (ascending)
val sortedResults = scoredResults.sortedWith { o1, o2 ->
val scoreCompare = o2.score.compareTo(o1.score)
if (scoreCompare != 0) scoreCompare
else o1.appName.compareTo(o2.appName, ignoreCase = true)
}
val sortedResults = scoredResults.sortedByDescending { it.score }

val finalAdapterItems = ArrayList<Any>()
val adapterItemClass = BASE_ADAPTER_ITEM_CLASS.toClass(appClassLoader)
val appInfoClass = APP_INFO_CLASS.toClass(appClassLoader)
sortedResults.forEach { result ->

sortedResults.forEach { result ->
try {
val adapterItem = adapterItemClass.method {
val adapterItem = adapterItemClass.method {
name = "asApp"
param(appInfoClass)
param(appInfoClass)
modifiers { isStatic }
}.get().call(result.appInfo)

if (adapterItem != null) {
finalAdapterItems.add(adapterItem)
}
} catch (e: Throwable) {
Log.e(TAG, "[FuzzySearch] Error converting ${result.appName}: ${e.message}")
}
}

return finalAdapterItems
}
}
5 changes: 4 additions & 1 deletion gradle/sweet-dependency/sweet-dependency-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,7 @@ libraries:
version: 3.6.1
junit:
junit:
version: 4.13.2
version: 4.13.2
me.xdrop:
fuzzywuzzy:
version: 1.4.0