refactor: simplify sim toolkit design #296

Merged
PeterCxy merged 1 commit from septs/OpenEUICC:simtoolkit into master 2026-02-24 00:26:21 +01:00
5 changed files with 40 additions and 73 deletions

View file

@ -34,7 +34,7 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.open_sim_toolkit).apply {
intent = stk[slotId]?.intent
intent = stk[slotId]
isVisible = intent != null
}
}

View file

@ -3,7 +3,6 @@ package im.angry.openeuicc.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
@ -13,84 +12,52 @@ import im.angry.openeuicc.core.EuiccChannelManager
class SIMToolkit(private val context: Context) {
private val slots = buildMap {
fun getComponentNames(@ArrayRes id: Int) = context.resources
.getStringArray(id).mapNotNull(ComponentName::unflattenFromString).toSet()
put(-1, getComponentNames(R.array.sim_toolkit_slot_selection))
put(0, getComponentNames(R.array.sim_toolkit_slot_1))
put(1, getComponentNames(R.array.sim_toolkit_slot_2))
fun getIntents(@ArrayRes id: Int) = context.resources.getStringArray(id)
.mapNotNull(ComponentName::unflattenFromString)
.map(Intent::makeMainActivity)
put(-1, getIntents(R.array.sim_toolkit_slot_selection))
put(0, getIntents(R.array.sim_toolkit_slot_1))
put(1, getIntents(R.array.sim_toolkit_slot_2))
}
val intents: Iterable<Intent?>
get() = listOf(get(0)?.intent, get(1)?.intent)
get() = listOf(get(0), get(1))
operator fun get(slotId: Int): Slot? = when (slotId) {
-1, EuiccChannelManager.USB_CHANNEL_ID -> null
else -> Slot(context.packageManager, buildSet {
addAll(slots.getOrDefault(slotId, emptySet()))
addAll(slots.getOrDefault(-1, emptySet()))
})
operator fun get(slotId: Int): Intent? {
if (slotId == -1 || slotId == EuiccChannelManager.USB_CHANNEL_ID) return null
val intents = (slots[slotId] ?: emptyList()) + slots[-1]!!
val packageNames = intents.mapNotNull(Intent::getPackage).toSet()
return getIntent(context.packageManager, intents) // try to find an exported activity first
?: getLaunchIntent(context.packageManager, packageNames) // fallback to launch intent
?: getDisabledPackageIntent(context.packageManager, packageNames) // app settings if disabled
}
data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
private val packageNames: Iterable<String>
get() = components.map(ComponentName::getPackageName).toSet()
.filter(packageManager::isInstalledApp)
private val launchIntent: Intent?
get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
private val activities: Iterable<ComponentName>
get() = packageNames.flatMap(packageManager::getActivities)
.filter(ActivityInfo::exported).map { ComponentName(it.packageName, it.name) }
private fun getActivityIntent(): Intent? {
for (activity in activities) {
if (!components.contains(activity)) continue
if (isDisabledState(packageManager.getComponentEnabledSetting(activity))) continue
return Intent.makeMainActivity(activity)
}
return launchIntent
}
private fun getDisabledPackageIntent(): Intent? {
val disabledPackageName = packageNames
.find { isDisabledState(packageManager.getApplicationEnabledSetting(it)) }
?: return null
val uri = Uri.fromParts("package", disabledPackageName, null)
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
}
val intent: Intent?
get() {
val intent = getActivityIntent() ?: getDisabledPackageIntent() ?: return null
if (intent.resolveActivity(packageManager) == null) return null
return intent
}
}
fun isSelection(intent: Intent) =
slots.getOrDefault(-1, emptySet()).contains(intent.component)
fun isSelection(intent: Intent) = intent in slots[-1]!!
companion object {
fun getDisabledPackageName(intent: Intent?): String? {
if (intent?.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) return null
return intent.data?.schemeSpecificPart
return intent.data!!.schemeSpecificPart
}
}
}
private fun isDisabledState(state: Int) = when (state) {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
else -> false
private fun getIntent(packageManager: PackageManager, intents: Iterable<Intent>) =
intents.firstOrNull { it.resolveActivityInfo(packageManager, 0)?.exported ?: false }
private fun getLaunchIntent(packageManager: PackageManager, packageNames: Iterable<String>) =
packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntentForPackage)
private fun getDisabledPackageIntent(packageManager: PackageManager, packageNames: Iterable<String>): Intent? {
val packageName = packageNames.firstOrNull(packageManager::isDisabledState) ?: return null
val uri = Uri.fromParts("package", packageName, null)
return Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
}
private fun PackageManager.isInstalledApp(packageName: String) = try {
getPackageInfo(packageName, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
}
private fun PackageManager.getActivities(packageName: String) =
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES).activities?.toList() ?: emptyList()
private fun PackageManager.isDisabledState(packageName: String) =
when (getApplicationEnabledSetting(packageName)) {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
else -> false
}

View file

@ -2,7 +2,7 @@
<string name="compatibility_check">兼容性检查</string>
<string name="open_sim_toolkit">打开 SIM 卡应用程序</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 已拷贝到剪贴板</string>
<string name="toast_prompt_to_enable_sim_toolkit">请启用您的“%s”应用程序</string>
<string name="toast_prompt_to_enable_sim_toolkit">请启用您的 “%s” 应用程序</string>
<string name="quick_compatibility">简易兼容性检测</string>
<string name="quick_compatibility_compatible">您的手机可以管理兼容 %s 的卡片</string>
<string name="quick_compatibility_not_compatible">您的手机与 %s 不兼容</string>
@ -14,4 +14,4 @@
<string name="quick_compatibility_button_continue">继续</string>
<string name="quick_compatibility_skip">不再显示此消息</string>
<string name="quick_compatibility_unknown">未知</string>
</resources>
</resources>

View file

@ -2,7 +2,7 @@
<string name="compatibility_check">相容性檢查</string>
<string name="open_sim_toolkit">啟動 SIM 卡應用程式</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 已複製到剪貼簿</string>
<string name="toast_prompt_to_enable_sim_toolkit">請啟用您的“%s”應用程式</string>
<string name="toast_prompt_to_enable_sim_toolkit">請啟用您的「%s」應用程式</string>
<string name="quick_compatibility">簡易相容性檢測</string>
<string name="quick_compatibility_compatible">您的手機可以管理相容 %s 的卡片</string>
<string name="quick_compatibility_not_compatible">您的手機與 %s 不相容</string>
@ -14,4 +14,4 @@
<string name="quick_compatibility_button_continue">繼續</string>
<string name="quick_compatibility_skip">不再顯示此訊息</string>
<string name="quick_compatibility_unknown">未知</string>
</resources>
</resources>

View file

@ -4,8 +4,8 @@
<string name="channel_name_format_unpriv_se" translatable="false">SIM %d, SE %d</string>
<string name="compatibility_check">Compatibility Check</string>
<string name="open_sim_toolkit">Open SIM Toolkit</string>
<string name="shortcut_sim_toolkit">SIM Toolkit</string>
<string name="shortcut_sim_toolkit_with_slot">SIM Toolkit #%d</string>
<string name="shortcut_sim_toolkit" translatable="false">SIM Toolkit</string>
<string name="shortcut_sim_toolkit_with_slot" translatable="false">SIM Toolkit #%d</string>
<!-- Settings -->
<string name="pref_developer_ara_m" translatable="false">ARA-M SHA-1</string>