Fix some HMA issues and add new functions (#33) with translation updates (#32)

- Fix AppSettingsFragment crash for uninstalled apps
- Remove package from search query if uninstalled
- Refresh + sort list when the package status changed
- Add "Clear uninstalled app configs" option
- Remove LSPlant dependent apps from suspicious apps list
- Fix some of detections by reverting dev options changes, but do not remove them
- Optimize imports
This commit is contained in:
Furkan Karcıoğlu 2025-10-28 12:21:51 +03:00 • committed by GitHub
commit 5c4205303e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 170 additions and 40 deletions

View file

@ -59,5 +59,12 @@ jobs:
- name: Build debug
id: buildDebug
run: |
./gradlew :app:assembleDebug
./gradlew assembleDebug
echo "debugName=$(ls app/build/outputs/apk/debug/*.apk | awk -F '(/|.apk)' '{print $6}')" >> $GITHUB_OUTPUT
- name: Upload debug
if: success()
uses: actions/upload-artifact@v4
with:
name: ${{ steps.buildDebug.outputs.debugName }}
path: "app/build/outputs/apk/debug/*.apk"

View file

@ -5,6 +5,7 @@ import android.util.Log
import icu.nullptr.hidemyapplist.common.JsonConfig
import icu.nullptr.hidemyapplist.hmaApp
import icu.nullptr.hidemyapplist.ui.util.showToast
import icu.nullptr.hidemyapplist.util.PackageHelper
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.common.BuildConfig
import java.io.File
@ -179,4 +180,28 @@ object ConfigManager {
else config.scope[packageName] = appConfig
saveConfig()
}
fun clearUninstalledAppConfigs(onFinish: (success: Boolean) -> Unit) {
PackageHelper.invalidateCache { throwable ->
if (throwable == null) {
val markedToRemove = mutableListOf<String>()
config.scope.keys.forEach { packageName ->
if (!PackageHelper.exists(packageName)) {
markedToRemove.add(packageName)
}
}
if (markedToRemove.isNotEmpty()) {
markedToRemove.forEach { config.scope.remove(it) }
saveConfig()
}
ServiceClient.log(Log.INFO, TAG, "Pruned ${markedToRemove.size} app config(s)")
onFinish(true)
} else {
onFinish(false)
}
}
}
}

View file

@ -3,6 +3,7 @@ package icu.nullptr.hidemyapplist.ui.fragment
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.ui.adapter.AppManageAdapter
import icu.nullptr.hidemyapplist.ui.util.navigate
import icu.nullptr.hidemyapplist.util.PackageHelper
import org.frknkrc44.hma_oss.R
class AppManageFragment : AppSelectFragment() {
@ -10,7 +11,9 @@ class AppManageFragment : AppSelectFragment() {
override val firstComparator: Comparator<String> = Comparator.comparing(ConfigManager::isHideEnabled).reversed()
override val adapter = AppManageAdapter {
val args = AppSettingsFragmentArgs(it)
navigate(R.id.nav_app_settings, args.toBundle())
if (PackageHelper.exists(it)) {
val args = AppSettingsFragmentArgs(it)
navigate(R.id.nav_app_settings, args.toBundle())
}
}
}

View file

@ -50,7 +50,7 @@ abstract class AppSelectFragment : Fragment(R.layout.fragment_app_select) {
}
}
open fun invalidateCache() {
protected open fun invalidateCache() {
PackageHelper.invalidateCache()
}
@ -81,6 +81,7 @@ abstract class AppSelectFragment : Fragment(R.layout.fragment_app_select) {
PrefManager.appFilter_reverseOrder = item.isChecked
}
}
sortList()
}
@ -144,8 +145,12 @@ abstract class AppSelectFragment : Fragment(R.layout.fragment_app_select) {
lifecycleScope.launch {
PackageHelper.isRefreshing
.flowWithLifecycle(lifecycle)
.collect {
binding.swipeRefresh.isRefreshing = it
.collect { isRefreshing ->
binding.swipeRefresh.isRefreshing = isRefreshing
if (!isRefreshing) {
sortList()
}
}
}

View file

@ -9,6 +9,7 @@ import android.view.WindowInsets
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceDataStore
@ -17,8 +18,8 @@ import androidx.preference.SwitchPreferenceCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import icu.nullptr.hidemyapplist.common.PropertyUtils
import icu.nullptr.hidemyapplist.common.Constants
import icu.nullptr.hidemyapplist.common.PropertyUtils
import icu.nullptr.hidemyapplist.hmaApp
import icu.nullptr.hidemyapplist.service.ConfigManager
import icu.nullptr.hidemyapplist.service.PrefManager
@ -30,6 +31,7 @@ import icu.nullptr.hidemyapplist.ui.util.showToast
import icu.nullptr.hidemyapplist.util.ConfigUtils.Companion.getLocale
import icu.nullptr.hidemyapplist.util.LangList
import icu.nullptr.hidemyapplist.util.SuUtils
import kotlinx.coroutines.launch
import org.frknkrc44.hma_oss.R
import org.frknkrc44.hma_oss.databinding.FragmentSettingsBinding
import java.util.Locale
@ -287,6 +289,40 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), PreferenceFragmen
}
}
findPreference<Preference>("clearUninstalledPackageConfigs")?.apply {
setOnPreferenceClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_clear_uninstalled_app_configs)
.setMessage(R.string.settings_no_undone_warning)
.setPositiveButton(android.R.string.ok) { _, _ ->
val progressDialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.settings_clear_uninstalled_app_configs)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
progressDialog.show()
ConfigManager.clearUninstalledAppConfigs { isSuccess ->
lifecycleScope.launch {
progressDialog.dismiss()
if (isSuccess) {
showToast(android.R.string.ok)
}
}
}
}
.setNegativeButton(android.R.string.cancel) { _, _ ->
// do nothing
}
.setCancelable(false)
.show()
true
}
}
findPreference<SwitchPreferenceCompat>("blackDarkTheme")?.apply {
isEnabled = findPreference<SwitchPreferenceCompat>("systemWallpaper")?.isChecked != true
setOnPreferenceChangeListener { _, _ ->

View file

@ -1,16 +1,14 @@
package icu.nullptr.hidemyapplist.util
import android.content.pm.ApplicationInfo
import android.content.pm.IPackageManager
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import icu.nullptr.hidemyapplist.common.Constants
import icu.nullptr.hidemyapplist.hmaApp
import icu.nullptr.hidemyapplist.service.PrefManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -19,7 +17,6 @@ import java.text.Collator
import java.util.Locale
object PackageHelper {
class PackageCache(
val info: PackageInfo,
val label: String,
@ -61,26 +58,22 @@ object PackageHelper {
}
}
private lateinit var ipm: IPackageManager
private lateinit var pm: PackageManager
private val packageCache = MutableSharedFlow<Map<String, PackageCache>>(replay = 1)
private val mAppList = MutableSharedFlow<List<String>>(replay = 1)
val appList: SharedFlow<List<String>> = mAppList
val appList = MutableSharedFlow<List<String>>(replay = 1)
private val mRefreshing = MutableSharedFlow<Boolean>(replay = 1)
val isRefreshing: SharedFlow<Boolean> = mRefreshing
val isRefreshing = MutableSharedFlow<Boolean>(replay = 1)
init {
// TODO: PackageManagerDelegate
pm = hmaApp.packageManager
invalidateCache()
}
fun invalidateCache() {
fun invalidateCache(
onFinished: ((Throwable?) -> Unit)? = null
) {
hmaApp.globalScope.launch {
mRefreshing.emit(true)
isRefreshing.emit(true)
val cache = withContext(Dispatchers.IO) {
val pm = hmaApp.packageManager
val packages = pm.getInstalledPackages(0)
mutableMapOf<String, PackageCache>().also {
for (packageInfo in packages) {
@ -94,8 +87,12 @@ object PackageHelper {
}
}
packageCache.emit(cache)
mAppList.emit(cache.keys.toList())
mRefreshing.emit(false)
appList.emit(cache.keys.toList())
isRefreshing.emit(false)
}.apply {
if (onFinished != null) {
invokeOnCompletion(onFinished)
}
}
}
@ -107,27 +104,37 @@ object PackageHelper {
PrefManager.SortMethod.BY_UPDATE_TIME -> Comparators.byUpdateTime
}
if (PrefManager.appFilter_reverseOrder) comparator = comparator.reversed()
val list = mAppList.first().sortedWith(firstComparator.then(comparator))
mAppList.emit(list)
val list = appList.first().sortedWith(firstComparator.then(comparator))
appList.emit(list)
}
private suspend fun getCacheNoThrow() = try {
packageCache.first()
} catch (_: Throwable) {
mapOf()
}
fun exists(packageName: String) = runBlocking {
packageCache.first().contains(packageName)
getCacheNoThrow().contains(packageName)
}
fun loadPackageInfo(packageName: String): PackageInfo = runBlocking {
packageCache.first()[packageName]!!.info
getCacheNoThrow()[packageName]!!.info
}
fun loadAppLabel(packageName: String): String = runBlocking {
packageCache.first()[packageName]!!.label
getCacheNoThrow()[packageName]?.label ?: packageName
}
fun loadAppIcon(packageName: String): Bitmap = runBlocking {
packageCache.first()[packageName]!!.icon
getCacheNoThrow()[packageName]?.icon ?:
BitmapFactory.decodeResource(
hmaApp.resources,
android.R.drawable.sym_def_app_icon
)
}
fun isSystem(packageName: String): Boolean = runBlocking {
packageCache.first()[packageName]?.info?.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0
getCacheNoThrow()[packageName]?.info?.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:padding="@dimen/item_padding_vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:indeterminate="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -158,6 +158,9 @@
<string name="settings_disable_activity_launch_protection">Disable Activity launch protection</string>
<string name="settings_disable_activity_launch_protection_summary">This toggle will lift all startActivity protections. If you\'re unsure, don\'t enable it.</string>
<string name="settings_system_wallpaper">Use system wallpaper</string>
<string name="settings_clear_uninstalled_app_configs">Clear uninstalled app configs</string>
<string name="settings_clear_uninstalled_app_configs_summary">This option allows you to create a config without remnants but you\'ll have to reconfigure apps when you installed them again.</string>
<string name="settings_no_undone_warning">This action cannot be undone.</string>
<string name="settings_language">Language</string>
<string name="settings_translate">Participate in translation</string>

View file

@ -82,6 +82,11 @@
android:key="hideIcon"
android:summary="@string/settings_hide_icon_summary"
android:title="@string/settings_hide_icon" />
<Preference
android:icon="@drawable/outline_delete_24"
android:key="clearUninstalledPackageConfigs"
android:summary="@string/settings_clear_uninstalled_app_configs_summary"
android:title="@string/settings_clear_uninstalled_app_configs" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_service">

View file

@ -19,6 +19,7 @@ fun String.execute(currentWorkingDir: File = file("./")): String {
val localProperties = Properties()
localProperties.load(file("local.properties").inputStream())
val ciBuild = providers.environmentVariable("CI").isPresent
val officialBuild by extra(localProperties.getProperty("officialBuild", "false") == "true")
@Suppress("unused")
@ -30,10 +31,15 @@ val crowdinApiKey: String by extra(localProperties.getProperty("crowdinApiKey",
fun getUncommittedSuffix(): String {
if (officialBuild) return ""
if (ciBuild) {
val headRefVal = providers.environmentVariable("GITHUB_HEAD_REF").orElse("HEAD").get()
return "-$headRefVal"
}
var returnedVal = ""
try {
val branch = "git symbolic-ref HEAD".execute().split("/").last()
val branch = "git rev-parse --abbrev-ref HEAD".execute().split("/").last()
if (branch != "master") {
returnedVal += "-$branch"
}
@ -48,7 +54,7 @@ fun getUncommittedSuffix(): String {
}
val gitHasUncommittedSuffix = getUncommittedSuffix()
val gitCommitCount = "git rev-list HEAD --count".execute().toInt()
val gitCommitCount = "git rev-list refs/remotes/origin/master --count".execute().toInt()
// 432 is the count of commits before license changed
val gitCommitCountAfterOss = gitCommitCount - 432

View file

@ -29,9 +29,16 @@ class SuspiciousAppsPreset : BasePreset("sus_apps") {
"com.amaze.filemanager",
)
val libNames = arrayOf(
"liblsplant.so",
/*
val libNames = arrayOf<String>(
// Note that LSPlant is used in some of games too
// I will not add it as suspicious app indicator anymore
//
// "liblsplant.so",
// TODO: Add more suspicious apps by checking for libs
)
*/
val assetNames = arrayOf(
// ~All possible APK editors
@ -101,7 +108,7 @@ class SuspiciousAppsPreset : BasePreset("sus_apps") {
}
ZipFile(appInfo.sourceDir).use { zipFile ->
if (findAppsFromLibs(zipFile, libNames) || findAppsFromAssets(zipFile, assetNames)) {
if (/*findAppsFromLibs(zipFile, libNames) ||*/ findAppsFromAssets(zipFile, assetNames)) {
return true
}
}

View file

@ -1,6 +1,14 @@
package icu.nullptr.hidemyapplist.common.settings_presets
abstract class BasePreset(val name: String) {
/*
companion object {
const val SETTINGS_GLOBAL = "global"
const val SETTINGS_SYSTEM = "system"
const val SETTINGS_SECURE = "secure"
}
*/
abstract val settingsKVPairs: List<ReplacementItem>
fun getSpoofedValue(key: String) = settingsKVPairs.firstOrNull { it.name == key }

View file

@ -16,6 +16,7 @@ class DeveloperOptionsPreset : BasePreset("dev_options") {
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
"0",
),
/*
ReplacementItem(
"debug.force_rtl",
"0"
@ -100,5 +101,6 @@ class DeveloperOptionsPreset : BasePreset("dev_options") {
"non_persistent_mac_randomization_force_enabled",
"0",
),
*/
)
}

View file

@ -2,10 +2,12 @@ package icu.nullptr.hidemyapplist.common.settings_presets
data class ReplacementItem(
val name: String,
val value: String?
val value: String?,
// val database: String = BasePreset.SETTINGS_GLOBAL,
) {
override fun toString() = "ReplacementItem {" +
" 'name': '$name'," +
"'value': '$value'" +
override fun toString() = "ReplacementItem { " +
"'name': '$name', " +
"'value': '$value', " +
// "'database': '$database'" +
" }"
}