diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbaa8f13..1fec2dc2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,7 @@ val props = val keyStoreFile = file(signFolder + props.getProperty("storeFile")) android { namespace = "com.aistra.hail" - compileSdk = 33 + compileSdk = 34 defaultConfig { applicationId = "com.aistra.hail" @@ -66,7 +66,6 @@ android { } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.biometric:biometric:1.1.0") @@ -76,13 +75,14 @@ dependencies { implementation("androidx.preference:preference-ktx:1.2.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.work:work-runtime-ktx:2.8.1") + implementation("com.belerweb:pinyin4j:2.5.1") implementation("com.google.android.material:material:1.9.0") implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3") implementation("dev.rikka.shizuku:api:13.1.4") implementation("dev.rikka.shizuku:provider:13.1.4") implementation("io.github.iamr0s:Dhizuku-API:2.4") implementation("me.zhanghai.android.appiconloader:appiconloader:1.5.0") - implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") - implementation("com.belerweb:pinyin4j:2.5.1") implementation("org.apache.commons:commons-text:1.10.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5c3394c9..f1417dfc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,8 @@ android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" /> + + HShizuku.setAppDisabled(packageName, frozen) HailData.MODE_SHIZUKU_HIDE -> HShizuku.setAppHidden(packageName, frozen) HailData.MODE_SHIZUKU_SUSPEND -> HShizuku.setAppSuspended(packageName, frozen) + HailData.MODE_ISLAND_HIDE -> HIsland.setAppHidden(packageName, frozen) + HailData.MODE_ISLAND_SUSPEND -> HIsland.setAppSuspended(packageName, frozen) else -> false } diff --git a/app/src/main/kotlin/com/aistra/hail/app/HailData.kt b/app/src/main/kotlin/com/aistra/hail/app/HailData.kt index 15724dd2..a70c4a21 100644 --- a/app/src/main/kotlin/com/aistra/hail/app/HailData.kt +++ b/app/src/main/kotlin/com/aistra/hail/app/HailData.kt @@ -32,6 +32,7 @@ object HailData { const val DHIZUKU = "dhizuku_" const val SU = "su_" const val SHIZUKU = "shizuku_" + const val ISLAND = "island_" const val DISABLE = "disable" const val HIDE = "hide" const val SUSPEND = "suspend" @@ -45,6 +46,8 @@ object HailData { const val MODE_SHIZUKU_DISABLE = SHIZUKU + DISABLE const val MODE_SHIZUKU_HIDE = SHIZUKU + HIDE const val MODE_SHIZUKU_SUSPEND = SHIZUKU + SUSPEND + const val MODE_ISLAND_HIDE = ISLAND + HIDE + const val MODE_ISLAND_SUSPEND = ISLAND + SUSPEND private const val TILE_ACTION = "tile_action" const val DYNAMIC_SHORTCUT_ACTION = "dynamic_shortcut_action" const val ACTION_NONE = "none" diff --git a/app/src/main/kotlin/com/aistra/hail/extensions/ViewExtensions.kt b/app/src/main/kotlin/com/aistra/hail/extensions/ViewExtensions.kt index 25e415ff..d491c6a4 100644 --- a/app/src/main/kotlin/com/aistra/hail/extensions/ViewExtensions.kt +++ b/app/src/main/kotlin/com/aistra/hail/extensions/ViewExtensions.kt @@ -9,7 +9,7 @@ val View.isRtl get() = layoutDirection == View.LAYOUT_DIRECTION_RTL /** * Very easy to apply insets to a view. - * */ + */ fun View.applyInsetsPadding( start: Boolean = false, end: Boolean = false, diff --git a/app/src/main/kotlin/com/aistra/hail/ui/home/PagerFragment.kt b/app/src/main/kotlin/com/aistra/hail/ui/home/PagerFragment.kt index a978b039..2da1c3f6 100644 --- a/app/src/main/kotlin/com/aistra/hail/ui/home/PagerFragment.kt +++ b/app/src/main/kotlin/com/aistra/hail/ui/home/PagerFragment.kt @@ -26,6 +26,8 @@ import com.aistra.hail.app.HailApi.addTag import com.aistra.hail.app.HailData import com.aistra.hail.databinding.DialogInputBinding import com.aistra.hail.databinding.FragmentPagerBinding +import com.aistra.hail.extensions.applyInsetsPadding +import com.aistra.hail.extensions.isLandscape import com.aistra.hail.ui.main.MainFragment import com.aistra.hail.utils.FuzzySearch import com.aistra.hail.utils.HPackages @@ -89,6 +91,12 @@ class PagerFragment : MainFragment(), PagerAdapter.OnItemClickListener, } } }) + + this.applyInsetsPadding( + start = !activity.isLandscape, + end = true, + bottom = activity.isLandscape + ) } binding.refresh.setOnRefreshListener { updateCurrentList() diff --git a/app/src/main/kotlin/com/aistra/hail/ui/settings/SettingsFragment.kt b/app/src/main/kotlin/com/aistra/hail/ui/settings/SettingsFragment.kt index 348a1459..a5f93043 100644 --- a/app/src/main/kotlin/com/aistra/hail/ui/settings/SettingsFragment.kt +++ b/app/src/main/kotlin/com/aistra/hail/ui/settings/SettingsFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.provider.Settings import android.view.* import android.widget.FrameLayout +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.content.res.AppCompatResources import androidx.core.app.NotificationManagerCompat import androidx.core.view.MenuHost @@ -42,6 +43,7 @@ import rikka.shizuku.Shizuku class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener, MenuProvider { + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {} override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { @@ -292,6 +294,27 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChan HUI.showToast(R.string.shizuku_missing) false } + + mode.startsWith(HailData.ISLAND) -> return runCatching{ + when{ + mode == HailData.MODE_ISLAND_HIDE && HIsland.freezePermissionGranted() -> true + mode == HailData.MODE_ISLAND_SUSPEND && HIsland.suspendPermissionGranted() -> true + else -> { + lifecycleScope.launch { + requestPermissionLauncher.launch(if(mode == HailData.MODE_ISLAND_HIDE) + HIsland.PERMISSION_FREEZE_PACKAGE + else + HIsland.PERMISSION_SUSPEND_PACKAGE + ) + } + false + } + } + }.getOrElse { + HLog.e(it) + HUI.showToast(R.string.permission_denied) + false + } } return true diff --git a/app/src/main/kotlin/com/aistra/hail/utils/FuzzySearch.kt b/app/src/main/kotlin/com/aistra/hail/utils/FuzzySearch.kt index 208f064d..b69dab44 100644 --- a/app/src/main/kotlin/com/aistra/hail/utils/FuzzySearch.kt +++ b/app/src/main/kotlin/com/aistra/hail/utils/FuzzySearch.kt @@ -2,64 +2,36 @@ package com.aistra.hail.utils import org.apache.commons.text.similarity.LevenshteinDistance -/**使用莱文斯坦距离 (Levenshtein distance)实现模糊搜索*/ +/** 使用莱文斯坦距离 (Levenshtein distance) 实现模糊搜索 */ object FuzzySearch { - private val levenshteinDistance: LevenshteinDistance = LevenshteinDistance() + private val levenshteinDistance = LevenshteinDistance() /** - * 两个字符串差异小于搜索字符串长度 且 搜索字符全部包含在搜索字符串中 则显示在搜索结果中 - * @param textToSearch 尝试匹配的字符串 - * @param query 用户输入字符串 - * */ - fun search(textToSearch: String?, query: String?): Boolean { - if (query.isNullOrEmpty()) { - return true - } - if (textToSearch.isNullOrEmpty()) { - return false - } - if (textToSearch.contains(query, true)) { - return true - } - val textToSearchUpp = textToSearch.uppercase() + * 两个字符串差异小于原始字符串长度 且 原始字符串依次包含输入字符串的每个字符 则显示在搜索结果中 + * @param raw 需要匹配的原始字符串 + * @param query 输入的字符串 + */ + fun search(raw: String?, query: String?): Boolean { + if (query.isNullOrEmpty()) return true + if (raw.isNullOrEmpty()) return false + if (raw.contains(query, true)) return true + val rawUpp = raw.uppercase() val queryUpp = query.uppercase() - val diff = levenshteinDistance.apply(textToSearchUpp, queryUpp) - val lenTextToSearch = textToSearchUpp.length - return diff < lenTextToSearch && containsInOrder(textToSearchUpp, queryUpp) + val diff = levenshteinDistance.apply(rawUpp, queryUpp) + return diff < rawUpp.length && containsInOrder(rawUpp, queryUpp) } - /** - * 判断一个字符串A是否依次包含另一个字符串B的每个字符,并且这些字符是按顺序从A的开头开始的 - * @param strA - * @param strB - * */ + /** 判断字符串A是否依次包含字符串B的每个字符 */ private fun containsInOrder(strA: String, strB: String): Boolean { var indexA = 0 // 用于跟踪字符串A中的位置 for (charB in strB) { // 在字符串A的当前位置之后查找字符charB val foundIndex = strA.indexOf(charB, indexA) // 如果未找到字符或者字符的位置不是当前位置,表示不包含按顺序的字符 - if (foundIndex == -1) { - return false - } + if (foundIndex == -1) return false // 移动到下一个位置,以便查找下一个字符 indexA = foundIndex + 1 } return true } - - private fun assertTrue(res: Boolean) { - if (!res){ - throw RuntimeException("测试失败!") - } - } - - @JvmStatic - fun main(args: Array) { - assertTrue(search("支付宝", "支")) - assertTrue(search("World Peace", "wp")) - assertTrue(search("World Peace", "pee")) - assertTrue(!search("World Peace", "dow")) - assertTrue(containsInOrder("小费计算器", "小器")) - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aistra/hail/utils/HIsland.kt b/app/src/main/kotlin/com/aistra/hail/utils/HIsland.kt new file mode 100644 index 00000000..9eabea3d --- /dev/null +++ b/app/src/main/kotlin/com/aistra/hail/utils/HIsland.kt @@ -0,0 +1,77 @@ +package com.aistra.hail.utils + +import android.app.Activity +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Handler +import android.os.HandlerThread +import androidx.core.content.ContextCompat +import com.aistra.hail.HailApp.Companion.app +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout + +object HIsland { + const val PERMISSION_FREEZE_PACKAGE = "com.oasisfeng.island.permission.FREEZE_PACKAGE" + const val PERMISSION_SUSPEND_PACKAGE = "com.oasisfeng.island.permission.SUSPEND_PACKAGE" + private const val ACTION_SUSPEND = "com.oasisfeng.island.action.SUSPEND" + private const val ACTION_UNSUSPEND = "com.oasisfeng.island.action.UNSUSPEND" + private const val ACTION_FREEZE = "com.oasisfeng.island.action.FREEZE" + private const val ACTION_UNFREEZE = "com.oasisfeng.island.action.UNFREEZE" + private const val EXTRA_CALLER_ID = "caller" + + private val thread by lazy { HandlerThread("HIsland").apply { start() } } + private val handler by lazy { Handler(thread.looper) } + + fun freezePermissionGranted(): Boolean { + return ContextCompat.checkSelfPermission( + app, PERMISSION_FREEZE_PACKAGE + ) == PackageManager.PERMISSION_GRANTED + } + + fun suspendPermissionGranted(): Boolean { + return ContextCompat.checkSelfPermission( + app, PERMISSION_SUSPEND_PACKAGE + ) == PackageManager.PERMISSION_GRANTED + } + + private fun setAppFrozen(packageName: String, action: String): Boolean { + val intent = Intent(action).apply { + data = Uri.fromParts("package", packageName, null) + setPackage("com.oasisfeng.island") + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + putExtra( + EXTRA_CALLER_ID, + PendingIntent.getActivity(app, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + ) + } + val result = CompletableDeferred() + app.sendOrderedBroadcast( + intent, null, object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (resultCode != Activity.RESULT_OK) { + HLog.i("HIsland", resultData) + } + result.complete(resultCode == Activity.RESULT_OK) + } + }, handler, Activity.RESULT_OK, null, null + ) + return runBlocking { + runCatching { + withTimeout(500L) { + result.await() + } + }.getOrElse { false } + } + } + + fun setAppHidden(packageName: String, hidden: Boolean): Boolean = + HTarget.N && setAppFrozen(packageName, if (hidden) ACTION_FREEZE else ACTION_UNFREEZE) + + fun setAppSuspended(packageName: String, suspended: Boolean): Boolean = + HTarget.N && setAppFrozen(packageName, if (suspended) ACTION_SUSPEND else ACTION_UNSUSPEND) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aistra/hail/utils/PinyinSearch.kt b/app/src/main/kotlin/com/aistra/hail/utils/PinyinSearch.kt index 8b11fcaf..6ebbcecf 100644 --- a/app/src/main/kotlin/com/aistra/hail/utils/PinyinSearch.kt +++ b/app/src/main/kotlin/com/aistra/hail/utils/PinyinSearch.kt @@ -3,77 +3,61 @@ package com.aistra.hail.utils import net.sourceforge.pinyin4j.PinyinHelper import java.util.Locale - -/**中文拼音搜索类*/ +/** 中文拼音搜索类 */ object PinyinSearch { /** - * 分别使用首字母和全拼进行匹配,满足条件之一就返回true, - * 如果当前语言不是中文就直接返回false - * @param textToSearch 需要匹配的字符串 - * @param query 用户输入 - * */ - fun searchPinyinAll(textToSearch: String?, query: String?): Boolean { - if (query.isNullOrEmpty()) { - return true - } - if (textToSearch.isNullOrEmpty()) { - return false - } - val language = Locale.getDefault().language - return if (language.equals(Locale.CHINESE.language)) { - searchCap(textToSearch, query) || searchAllSpell(textToSearch, query) - } else { - false - } + * 当前语言是中文时使用首字母和全拼进行搜索,满足任一条件则显示在搜索结果中 + * @param raw 需要匹配的原始字符串 + * @param query 输入的字符串 + */ + fun searchPinyinAll(raw: String?, query: String?): Boolean { + if (query.isNullOrEmpty()) return true + if (raw.isNullOrEmpty()) return false + return if (Locale.getDefault().language != Locale.CHINESE.language) false + else searchCap(raw, query) || searchAllSpell(raw, query) } /** - * 根据首字母进行搜索 - * 比如搜索”计算器“ 只需要输入 ”jsq“ - * */ - fun searchCap(appName: String, pinyinCap: String): Boolean { - if (pinyinCap.length < 80) { - for (index in getNameStringList(appName)) { - if (index.contains(pinyinCap, true)) { - return true - } + * 根据拼音首字母进行搜索 + * 如搜索“计算器”时只需输入 "jsq" + */ + private fun searchCap(raw: String, pinyinCap: String): Boolean { + if (pinyinCap.length > 8) return false // "最强多媒体播放器".length + for (index in getNameStringList(raw)) { + if (index.contains(pinyinCap, true)) { + return true } - return false - } else { - return false } + return false } /** * 根据全部拼音进行搜索 - * 比如搜索 计算器 只需要输入 "jisuanqi" - * */ - fun searchAllSpell(appName: String, pinyinAll: String): Boolean { - if (pinyinAll.length < 30) { - for (index in getNameStringPinyinAll(appName)) { - if (index.contains(pinyinAll, true)) { - return true - } + * 如搜索“计算器”时只需输入 "jisuanqi" + */ + private fun searchAllSpell(raw: String, pinyinAll: String): Boolean { + if (pinyinAll.length > 48) return false // "chuang".length * 8 + for (index in getNameStringPinyinAll(raw)) { + if (index.contains(pinyinAll, true)) { + return true } - return false - } else { - return false } + return false } - fun getNameStringPinyinAll(target: String): ArrayList { + private fun getNameStringPinyinAll(target: String): ArrayList { val res = ArrayList() getNameCapListPinyinAll(Array(target.length) { "" }, 0, target, res) return res } - fun getNameStringList(target: String): ArrayList { + private fun getNameStringList(target: String): ArrayList { val res = ArrayList() getNameCapList(CharArray(target.length), 0, target, res) return res } - fun getNameCapList( + private fun getNameCapList( capList: CharArray, currentIndex: Int, target: String, result: ArrayList ) { if (currentIndex == target.length - 1) { @@ -109,7 +93,7 @@ object PinyinSearch { } } - fun getNameCapListPinyinAll( + private fun getNameCapListPinyinAll( fullList: Array, currentIndex: Int, target: String, result: ArrayList ) { if (currentIndex == target.length - 1) { diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 682cc413..c730518b 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -32,8 +32,10 @@ + app:contentPadding="-18dp" + app:srcCompat="@drawable/ic_launcher_foreground" /> Superusuario - Ocultar F-Droid Omitir + Isla - Ocultar + Isla - Suspender \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index dbfd4d40..c8f7a5fd 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -138,6 +138,8 @@ 终端 Dhizuku - 隐藏 Dhizuku - 暂停 + 炼妖壶 - 隐藏 + 炼妖壶 - 暂停 超级用户 - 隐藏 F-Droid 跳过 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 703018d9..2a6b19f8 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -41,6 +41,8 @@ @string/mode_dhizuku_suspend @string/mode_owner_hide @string/mode_owner_suspend + @string/mode_island_hide + @string/mode_island_suspend default @@ -54,6 +56,8 @@ dhizuku_suspend owner_hide owner_suspend + island_hide + island_suspend @string/donate_alipay diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f609918f..af9fef0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,4 +140,6 @@ Superuser - Hide F-Droid Skip + Island - Hide + Island - Suspend \ No newline at end of file