Browse Source

feat: prompt to enable disabled sim toolkit app (#153)

<video src="/attachments/fb9f210c-5960-4889-ba6a-dba4aa085a12" title="screen-20250305-130942.mp4" controls></video>

Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/153
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
septs 11 months ago
parent
commit
65c7f8de83

+ 20 - 3
app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedEuiccManagementFragment.kt

@@ -1,7 +1,10 @@
 package im.angry.openeuicc.ui
 
+import android.content.pm.PackageManager
+import android.provider.Settings
 import android.view.Menu
 import android.view.MenuInflater
+import android.widget.Toast
 import im.angry.easyeuicc.R
 import im.angry.openeuicc.util.SIMToolkit
 import im.angry.openeuicc.util.newInstanceEuicc
@@ -24,8 +27,22 @@ class UnprivilegedEuiccManagementFragment : EuiccManagementFragment() {
         super.onCreateOptionsMenu(menu, inflater)
         inflater.inflate(R.menu.fragment_sim_toolkit, menu)
         menu.findItem(R.id.open_sim_toolkit).apply {
-            isVisible = stk.isAvailable(slotId)
-            intent = stk.intent(slotId)
+            val slot = stk[slotId] ?: return@apply
+            isVisible = slot.intent != null
+            setOnMenuItemClickListener {
+                val intent = slot.intent ?: return@setOnMenuItemClickListener false
+                if (intent.action == Settings.ACTION_APPLICATION_DETAILS_SETTINGS) {
+                    val packageName = intent.data!!.schemeSpecificPart
+                    val label = requireContext().packageManager.getApplicationLabel(packageName)
+                    val message = requireContext().getString(R.string.toast_prompt_to_enable_sim_toolkit, label)
+                    Toast.makeText(context, message, Toast.LENGTH_LONG).show()
+                }
+                startActivity(intent)
+                true
+            }
         }
     }
-}
+}
+
+private fun PackageManager.getApplicationLabel(packageName: String): CharSequence =
+    getApplicationLabel(getApplicationInfo(packageName, 0))

+ 62 - 40
app-unpriv/src/main/java/im/angry/openeuicc/util/SIMToolkit.kt

@@ -3,65 +3,87 @@ 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
+import android.widget.Toast
 import androidx.annotation.ArrayRes
 import im.angry.easyeuicc.R
 import im.angry.openeuicc.core.EuiccChannelManager
 
 class SIMToolkit(private val context: Context) {
-    private val slotSelection = getComponentNames(R.array.sim_toolkit_slot_selection)
-
     private val slots = buildMap {
+        fun getComponentNames(@ArrayRes id: Int) = context.resources
+            .getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
+        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))
     }
 
-    private val packageNames = buildSet {
-        addAll(slotSelection.map { it.packageName })
-        addAll(slots.values.flatten().map { it.packageName })
+    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()))
+        })
     }
 
-    private val activities = packageNames.flatMap(::getActivities).toSet()
+    data class Slot(private val packageManager: PackageManager, private val components: Set<ComponentName>) {
+        private val packageNames: Iterable<String>
+            get() = components.map(ComponentName::getPackageName).toSet()
 
-    private val launchIntent by lazy {
-        packageNames.firstNotNullOfOrNull(::getLaunchIntent)
-    }
+        private val launchIntent: Intent?
+            get() = packageNames.firstNotNullOfOrNull(packageManager::getLaunchIntent)
 
-    private fun getLaunchIntent(packageName: String) = try {
-        val pm = context.packageManager
-        pm.getLaunchIntentForPackage(packageName)
-    } catch (_: PackageManager.NameNotFoundException) {
-        null
-    }
+        private val activities: Iterable<ComponentName>
+            get() = packageNames.flatMap(packageManager::getActivities)
+                .filter(ActivityInfo::exported).map { ComponentName(it.packageName, it.name) }
 
-    private fun getActivities(packageName: String): List<ComponentName> {
-        return try {
-            val pm = context.packageManager
-            val packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
-            val activities = packageInfo.activities
-            if (activities.isNullOrEmpty()) return emptyList()
-            activities.filter { it.exported }.map { ComponentName(it.packageName, it.name) }
-        } catch (_: PackageManager.NameNotFoundException) {
-            emptyList()
+        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 getComponentNames(@ArrayRes id: Int) =
-        context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
+        private fun getDisabledPackageIntent(): Intent? {
+            val disabledPackageName = packageNames.find {
+                try {
+                    isDisabledState(packageManager.getApplicationEnabledSetting(it))
+                } catch (_: IllegalArgumentException) {
+                    false
+                }
+            }
+            if (disabledPackageName == null) return null
+            return Intent(
+                Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+                Uri.fromParts("package", disabledPackageName, null)
+            )
+        }
 
-    fun isAvailable(slotId: Int) = when (slotId) {
-        -1 -> false
-        EuiccChannelManager.USB_CHANNEL_ID -> false
-        else -> intent(slotId) != null
+        val intent: Intent?
+            get() = getActivityIntent() ?: getDisabledPackageIntent()
     }
+}
 
-    fun intent(slotId: Int): Intent? {
-        val components = slots.getOrDefault(slotId, emptySet()) + slotSelection
-        val intent = Intent(Intent.ACTION_MAIN, null).apply {
-            flags = Intent.FLAG_ACTIVITY_NEW_TASK
-            component = components.find(activities::contains)
-            addCategory(Intent.CATEGORY_LAUNCHER)
-        }
-        return if (intent.component != null) intent else launchIntent
-    }
+private fun isDisabledState(state: Int) = when (state) {
+    PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> true
+    PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> true
+    else -> false
+}
+
+private fun PackageManager.getLaunchIntent(packageName: String) = try {
+    getLaunchIntentForPackage(packageName)
+} catch (_: PackageManager.NameNotFoundException) {
+    null
+}
+
+private fun PackageManager.getActivities(packageName: String) = try {
+    getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
+        .activities?.toList() ?: emptyList()
+} catch (_: PackageManager.NameNotFoundException) {
+    emptyList()
 }

+ 1 - 0
app-unpriv/src/main/res/values/strings.xml

@@ -9,6 +9,7 @@
 
     <!-- Toast -->
     <string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
+    <string name="toast_prompt_to_enable_sim_toolkit">Please ENABLE your \"%s\" application</string>
 
     <!-- Compatibility Check Descriptions -->
     <string name="compatibility_check_system_features">System Features</string>