diff --git a/app/src/main/java/com/github/droidworksstudio/fuzzywuzzy/FuzzyFinder.kt b/app/src/main/java/com/github/droidworksstudio/fuzzywuzzy/FuzzyFinder.kt new file mode 100644 index 0000000..06d16ba --- /dev/null +++ b/app/src/main/java/com/github/droidworksstudio/fuzzywuzzy/FuzzyFinder.kt @@ -0,0 +1,66 @@ +package com.github.droidworksstudio.fuzzywuzzy + +import com.github.droidworksstudio.launcher.data.entities.AppInfo +import java.text.Normalizer +import java.util.* + +object FuzzyFinder { + fun scoreApp(app: AppInfo, searchChars: String, topScore: Int): Int { + val appChars = app.appName + + val fuzzyScore = calculateFuzzyScore( + normalizeString(appChars), + normalizeString(searchChars) + ) + + return (fuzzyScore * topScore).toInt() + } + + fun normalizeString(appLabel: String, searchChars: String): Boolean { + return (appLabel.contains(searchChars, true) or + Normalizer.normalize(appLabel, Normalizer.Form.NFD) + .replace(Regex("\\p{InCombiningDiacriticalMarks}+"), "") + .replace(Regex("[-_+,. ]"), "") + .contains(searchChars, true)) + } + + private fun normalizeString(input: String): String { + // Remove diacritical marks and special characters, and convert to uppercase + return input + .uppercase(Locale.getDefault()) + .replace(Regex("[\\p{InCombiningDiacriticalMarks}-_+,.]"), "") + } + + private fun calculateFuzzyScore(s1: String, s2: String): Float { + val m = s1.length + val n = s2.length + var matchCount = 0 + var s1Index = 0 + + // Iterate over each character in s2 and check if it exists in s1 + for (c2 in s2) { + var found = false + + // Start searching for c2 from the current s1Index + for (j in s1Index until m) { + if (s1[j] == c2) { + found = true + // Update s1Index to the next position for the next iteration + s1Index = j + 1 + break + } + } + + // If the current character in s2 is not found in s1, return a score of 0 + if (!found) { + return 0f + } + + // Increment the match count + matchCount++ + } + + // Calculate the score as the ratio of matched characters to the longer string length + return matchCount.toFloat() / maxOf(m, n) + } +} diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/data/dao/AppInfoDAO.kt b/app/src/main/java/com/github/droidworksstudio/launcher/data/dao/AppInfoDAO.kt index 39323f7..9b79fc5 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/data/dao/AppInfoDAO.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/data/dao/AppInfoDAO.kt @@ -43,8 +43,8 @@ interface AppInfoDAO { @Query("SELECT * FROM app WHERE is_lock = 1 ORDER BY app_order ASC") fun getLockAppsFlow(): Flow> - @Query("SELECT * FROM app WHERE app_name LIKE :query COLLATE NOCASE AND is_hidden = 0") - fun searchApps(query: String?): Flow> + @Query("SELECT * FROM app ORDER BY app_name COLLATE NOCASE ASC") + fun searchApps(): Flow> @Update suspend fun updateAppInfo(appInfo: AppInfo) diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt b/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt index 3856950..2633179 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt @@ -103,6 +103,14 @@ class PreferenceHelper @Inject constructor(@ApplicationContext context: Context) get() = prefs.getBoolean(Constants.AUTOMATIC_OPEN_APP, false) set(value) = prefs.edit().putBoolean(Constants.AUTOMATIC_OPEN_APP, value).apply() + var searchFromStart: Boolean + get() = prefs.getBoolean(Constants.SEARCH_FROM_START, true) + set(value) = prefs.edit().putBoolean(Constants.SEARCH_FROM_START, value).apply() + + var filterStrength: Int + get() = prefs.getInt(Constants.FILTER_STRENGTH, 25) + set(value) = prefs.edit().putInt(Constants.FILTER_STRENGTH, value).apply() + var homeAppAlignment: Int get() = prefs.getInt(Constants.HOME_APP_ALIGNMENT, Gravity.START) set(value) = prefs.edit().putInt(Constants.HOME_APP_ALIGNMENT, value).apply() diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/repository/AppInfoRepository.kt b/app/src/main/java/com/github/droidworksstudio/launcher/repository/AppInfoRepository.kt index 36db6c0..983f3a0 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/repository/AppInfoRepository.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/repository/AppInfoRepository.kt @@ -53,8 +53,8 @@ class AppInfoRepository @Inject constructor( } } - fun searchNote(query: String?): Flow> { - return appDao.searchApps(query) + fun searchNote(): Flow> { + return appDao.searchApps() } suspend fun updateFavoriteAppInfo(appInfo: AppInfo) = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt b/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt index 2dbf0ff..8a77e5a 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/ui/drawer/DrawFragment.kt @@ -24,6 +24,7 @@ import com.github.droidworksstudio.common.searchCustomSearchEngine import com.github.droidworksstudio.common.searchOnPlayStore import com.github.droidworksstudio.common.showKeyboard import com.github.droidworksstudio.common.showLongToast +import com.github.droidworksstudio.fuzzywuzzy.FuzzyFinder import com.github.droidworksstudio.launcher.R import com.github.droidworksstudio.launcher.adapter.drawer.DrawAdapter import com.github.droidworksstudio.launcher.data.entities.AppInfo @@ -35,6 +36,7 @@ import com.github.droidworksstudio.launcher.listener.OnItemClickedListener import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener import com.github.droidworksstudio.launcher.listener.ScrollEventListener import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment +import com.github.droidworksstudio.launcher.utils.Constants import com.github.droidworksstudio.launcher.viewmodel.AppViewModel import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -194,7 +196,7 @@ class DrawFragment : Fragment(), // Use repeatOnLifecycle to manage the lifecycle state repeatOnLifecycle(Lifecycle.State.CREATED) { val trimmedQuery = searchQuery.trim() - viewModel.searchAppInfo(trimmedQuery).collect { searchResults -> + viewModel.searchAppInfo().collect { searchResults -> val numberOfItemsLeft = searchResults.size val appResults = searchResults.firstOrNull() if (numberOfItemsLeft == 0 && !requireContext().searchOnPlayStore(trimmedQuery)) { @@ -211,26 +213,63 @@ class DrawFragment : Fragment(), } private fun searchApp(query: String) { - val searchQuery = "%$query%" - // Launch a coroutine tied to the lifecycle of the view viewLifecycleOwner.lifecycleScope.launch { // Repeat the block when the lifecycle is at least CREATED repeatOnLifecycle(Lifecycle.State.CREATED) { + val trimmedQuery = query.trim() + // Collect search results from the ViewModel - viewModel.searchAppInfo(searchQuery).collect { searchResults -> - val numberOfItemsLeft = searchResults.size - val appResults = searchResults.firstOrNull() + viewModel.searchAppInfo().collect { searchResults -> + // Filter and score results using FuzzyFinder + val filteredResults = searchResults + .map { appInfo -> + val score = FuzzyFinder.scoreApp(appInfo, trimmedQuery, Constants.FILTER_STRENGTH_MAX) + appInfo to score // Pairing app info with its score + } + .filter { it.second > 25 } // Only keep results with a positive score + .sortedByDescending { it.second } // Sort results by score, descending + + // Applying additional filtering based on preferences + val scoredApps = filteredResults.toMap() + + val finalResults = if (preferenceHelper.filterStrength >= 1) { + // Filtering based on score strength + if (preferenceHelper.searchFromStart) { + // Filter apps that start with the search query and score higher than the filter strength + scoredApps.filter { (app, _) -> + app.appName.startsWith(trimmedQuery, ignoreCase = true) + } + .filter { (_, score) -> score > preferenceHelper.filterStrength } + .map { it.key } + .toMutableList() + } else { + // Filter based on score strength alone + scoredApps.filterValues { it > preferenceHelper.filterStrength } + .keys + .toMutableList() + } + } else { + // If filter strength is less than 1, normalize app names for both cases + searchResults.filter { app -> + FuzzyFinder.normalizeString(app.appName, trimmedQuery) + }.toMutableList() + } + + + val numberOfItemsLeft = finalResults.size + val appResults = finalResults.firstOrNull() + when (numberOfItemsLeft) { 1 -> { appResults?.let { appInfo -> if (preferenceHelper.automaticOpenApp) observeBioAuthCheck(appInfo) } - drawAdapter.submitList(searchResults) + drawAdapter.submitList(finalResults) } else -> { - drawAdapter.submitList(searchResults) + drawAdapter.submitList(finalResults) } } } @@ -238,7 +277,6 @@ class DrawFragment : Fragment(), } } - private fun showSelectedApp(appInfo: AppInfo) { binding.searchViewText.setQuery("", false) diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsFeaturesFragment.kt b/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsFeaturesFragment.kt index ab59a18..48eca14 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsFeaturesFragment.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsFeaturesFragment.kt @@ -1,12 +1,17 @@ package com.github.droidworksstudio.launcher.ui.settings import android.content.Context +import android.os.Build import android.os.Bundle import android.util.Log +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.SeekBar import android.widget.TextView +import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -62,6 +67,7 @@ class SettingsFeaturesFragment : Fragment(), } // Called after the fragment view is created + @RequiresApi(Build.VERSION_CODES.O) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { navController = findNavController() // Set according to the system theme mode @@ -75,6 +81,7 @@ class SettingsFeaturesFragment : Fragment(), binding.apply { miscellaneousSearchEngineControl.text = preferenceHelper.searchEngines.getString(context) + miscellaneousFilterStrengthControl.text = "${preferenceHelper.filterStrength}" } val actions = listOf( @@ -118,10 +125,12 @@ class SettingsFeaturesFragment : Fragment(), binding.apply { automaticKeyboardSwitchCompat.isChecked = preferenceHelper.automaticKeyboard automaticOpenAppSwitchCompat.isChecked = preferenceHelper.automaticOpenApp + searchFromStartSwitchCompat.isChecked = preferenceHelper.searchFromStart lockSettingsSwitchCompat.isChecked = preferenceHelper.settingsLock } } + @RequiresApi(Build.VERSION_CODES.O) private fun observeClickListener() { setupSwitchListeners() @@ -149,6 +158,10 @@ class SettingsFeaturesFragment : Fragment(), miscellaneousSearchEngineControl.setOnClickListener { showSearchEngineDialog() } + + miscellaneousFilterStrengthControl.setOnClickListener { + showFilterStrengthDialog() + } } } @@ -163,19 +176,83 @@ class SettingsFeaturesFragment : Fragment(), // Map the enum values to their string representations val itemStrings = items.map { it.getString(context) }.toTypedArray() - val dialogBuilder = MaterialAlertDialogBuilder(context) - - dialogBuilder.setTitle(getString(R.string.settings_select_search_engine)) - dialogBuilder.setItems(itemStrings) { _, which -> - val selectedItem = items[which] - preferenceViewModel.setSearchEngine(selectedItem) - binding.miscellaneousSearchEngineControl.text = preferenceHelper.searchEngines.name + val dialogBuilder = MaterialAlertDialogBuilder(context).apply { + setTitle(getString(R.string.settings_select_search_engine)) + setItems(itemStrings) { _, which -> + val selectedItem = items[which] + preferenceViewModel.setSearchEngine(selectedItem) + binding.miscellaneousSearchEngineControl.text = preferenceHelper.searchEngines.name + } } // Assign the created dialog to launcherFontDialog searchEngineDialog = dialogBuilder.create() searchEngineDialog?.show() } + private var filterStrengthDialog: AlertDialog? = null + + @RequiresApi(Build.VERSION_CODES.O) + private fun showFilterStrengthDialog() { + // Dismiss any existing dialog to prevent multiple dialogs open simultaneously + filterStrengthDialog?.dismiss() + + var currentValue = preferenceHelper.filterStrength + + // Create a layout to hold the SeekBar and the value display + val seekBarLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + setPadding(16, 16, 16, 16) + + // TextView to display the current value + val valueText = TextView(context).apply { + text = "$currentValue" + textSize = 16f + gravity = Gravity.CENTER + } + + // SeekBar for horizontal number selection + val seekBar = SeekBar(context).apply { + min = Constants.FILTER_STRENGTH_MIN // Maximum value + max = Constants.FILTER_STRENGTH_MAX // Maximum value + progress = currentValue // Default value + setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + currentValue = progress + valueText.text = "$currentValue" + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + // Not used + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + // Not used + } + }) + } + + // Add TextView and SeekBar to the layout + addView(valueText) + addView(seekBar) + } + + // Create the dialog + val dialogBuilder = MaterialAlertDialogBuilder(context).apply { + setTitle(getString(R.string.settings_select_filter_strength)) + setView(seekBarLayout) // Add the slider directly to the dialog + setPositiveButton("ok") { _, _ -> + // Save the slider value when OK is pressed + preferenceViewModel.setFilterStrength(currentValue) + binding.miscellaneousFilterStrengthControl.text = "$currentValue" + } + setNegativeButton("cancel", null) + } + // Assign the created dialog to launcherFontDialog + filterStrengthDialog = dialogBuilder.create() + filterStrengthDialog?.show() + } + private var appSelectionDialog: AlertDialog? = null private fun showAppSelectionDialog(swipeType: Constants.Swipe) { @@ -191,18 +268,18 @@ class SettingsFeaturesFragment : Fragment(), val packageNames = installedApps.map { it.packageName } // Build and display the dialog - val dialogBuilder = MaterialAlertDialogBuilder(context) - dialogBuilder.setTitle("Select an App") - dialogBuilder.setItems(appNames) { _, which -> - val selectedPackageName = packageNames[which] - when (swipeType) { - Constants.Swipe.DoubleTap, - Constants.Swipe.Up, - Constants.Swipe.Down, - Constants.Swipe.Left, - Constants.Swipe.Right -> handleSwipeAction(swipeType, selectedPackageName) + val dialogBuilder = MaterialAlertDialogBuilder(context).apply { + setTitle("Select an App") + setItems(appNames) { _, which -> + val selectedPackageName = packageNames[which] + when (swipeType) { + Constants.Swipe.DoubleTap, + Constants.Swipe.Up, + Constants.Swipe.Down, + Constants.Swipe.Left, + Constants.Swipe.Right -> handleSwipeAction(swipeType, selectedPackageName) + } } - } // Assign the created dialog to launcherFontDialog @@ -222,6 +299,10 @@ class SettingsFeaturesFragment : Fragment(), preferenceViewModel.setAutoOpenApp(isChecked) } + searchFromStartSwitchCompat.setOnCheckedChangeListener { _, isChecked -> + preferenceViewModel.setSearchFromStart(isChecked) + } + lockSettingsSwitchCompat.setOnCheckedChangeListener { _, isChecked -> preferenceViewModel.setLockSettings(isChecked) } @@ -335,6 +416,7 @@ class SettingsFeaturesFragment : Fragment(), private fun dismissDialogs() { searchEngineDialog?.dismiss() + filterStrengthDialog?.dismiss() appSelectionDialog?.dismiss() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt b/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt index af358a0..464ee29 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt @@ -57,6 +57,9 @@ object Constants { const val AUTOMATIC_KEYBOARD = "AUTOMATIC_KEYBOARD" const val AUTOMATIC_OPEN_APP = "AUTOMATIC_OPEN_APP" + const val SEARCH_FROM_START = "SEARCH_FROM_START" + const val FILTER_STRENGTH = "FILTER_STRENGTH" + const val TOGGLE_SETTING_LOCK = "TOGGLE_SETTING_LOCK" const val HOME_DATE_ALIGNMENT = "HOME_DATE_ALIGNMENT" const val HOME_TIME_ALIGNMENT = "HOME_TIME_ALIGNMENT" @@ -71,8 +74,6 @@ object Constants { const val QUICKSETTINGS_MANAGER = "android.app.StatusBarManager" const val QUICKSETTINGS_METHOD = "expandSettingsPanel" - const val TOGGLE_SETTING_LOCK = "TOGGLE_SETTING_LOCK" - const val SEARCH_ENGINE = "SEARCH_ENGINE" const val URL_DUCK_SEARCH = "https://duckduckgo.com/?q=" const val URL_GOOGLE_SEARCH = "https://google.com/search?q=" @@ -88,6 +89,9 @@ object Constants { const val REQUEST_INSTALL_PERMISSION = 123 const val REQUEST_LOCATION_PERMISSION_CODE = 234 + const val FILTER_STRENGTH_MIN = 0 + const val FILTER_STRENGTH_MAX = 100 + const val BACKUP_WRITE = 987 const val BACKUP_READ = 876 diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/AppViewModel.kt b/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/AppViewModel.kt index 66c9b6c..20e1793 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/AppViewModel.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/AppViewModel.kt @@ -77,5 +77,5 @@ class AppViewModel @Inject constructor( } } - fun searchAppInfo(query: String?) = appInfoRepository.searchNote(query) + fun searchAppInfo() = appInfoRepository.searchNote() } diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt b/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt index 088cbce..927eeab 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt @@ -39,12 +39,14 @@ class PreferenceViewModel @Inject constructor( private val appTextSizeLiveData: MutableLiveData = MutableLiveData() private val batteryTextSizeLiveData: MutableLiveData = MutableLiveData() private val autoOpenAppsLiveData: MutableLiveData = MutableLiveData() + private val searchFromStartLiveData: MutableLiveData = MutableLiveData() private val autoKeyboardLiveData: MutableLiveData = MutableLiveData() private val lockSettingsLiveData: MutableLiveData = MutableLiveData() private val appPaddingSizeLiveData: MutableLiveData = MutableLiveData() private val weatherOrderNumberLiveData: MutableLiveData = MutableLiveData() private val batteryOrderNumberLiveData: MutableLiveData = MutableLiveData() + private val filterStrengthLiveData: MutableLiveData = MutableLiveData() private val searchEngineLiveData: MutableLiveData = MutableLiveData() private val launcherFontLiveData: MutableLiveData = MutableLiveData() @@ -234,6 +236,11 @@ class PreferenceViewModel @Inject constructor( autoOpenAppsLiveData.postValue((preferenceHelper.automaticOpenApp)) } + fun setSearchFromStart(searchFromStart: Boolean) { + preferenceHelper.searchFromStart = searchFromStart + searchFromStartLiveData.postValue((preferenceHelper.searchFromStart)) + } + fun setLockSettings(lockSettings: Boolean) { preferenceHelper.settingsLock = lockSettings lockSettingsLiveData.postValue((preferenceHelper.settingsLock)) @@ -244,6 +251,11 @@ class PreferenceViewModel @Inject constructor( searchEngineLiveData.postValue((preferenceHelper.searchEngines)) } + fun setFilterStrength(filterStrength: Int) { + preferenceHelper.filterStrength = filterStrength + filterStrengthLiveData.postValue((preferenceHelper.filterStrength)) + } + fun setLauncherFont(launcherFont: Constants.Fonts) { preferenceHelper.launcherFont = launcherFont launcherFontLiveData.postValue((preferenceHelper.launcherFont)) diff --git a/app/src/main/res/layout/fragment_settings_features.xml b/app/src/main/res/layout/fragment_settings_features.xml index 837a133..f4f73ed 100644 --- a/app/src/main/res/layout/fragment_settings_features.xml +++ b/app/src/main/res/layout/fragment_settings_features.xml @@ -122,6 +122,37 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index ebc6985..ce48e53 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,6 +1,6 @@ -