Compare commits

...

3 commits

Author SHA1 Message Date
99d9200c28 fix: omapi apdu interface (#152)
Reviewed-on: PeterCxy/OpenEUICC#152
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:19:16 +01:00
65c7f8de83 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: PeterCxy/OpenEUICC#153
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:18:56 +01:00
6c9063a761 chore: add zh-TW to locale-config (#155)
Reviewed-on: PeterCxy/OpenEUICC#155
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2025-03-05 14:18:03 +01:00
5 changed files with 94 additions and 57 deletions

View file

@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface
import java.util.concurrent.atomic.AtomicInteger
class OmapiApduInterface(
private val service: SEService,
@ -20,12 +21,8 @@ class OmapiApduInterface(
}
private lateinit var session: Session
private val channels = arrayOf<Channel?>(
null,
null,
null,
null,
)
private val index = AtomicInteger(0)
private val channels = mutableMapOf<Int, Channel>()
override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@ -44,21 +41,20 @@ class OmapiApduInterface(
override fun logicalChannelOpen(aid: ByteArray): Int {
val channel = session.openLogicalChannel(aid)
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" }
val index = channels.indexOf(null)
check(index != -1) { "No free logical channel slots" }
synchronized(channels) { channels[index] = channel }
return index
val handle = index.incrementAndGet()
synchronized(channels) { channels[handle] = channel }
return handle
}
override fun logicalChannelClose(handle: Int) {
val channel = channels.getOrNull(handle)
val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" }
if (channel.isOpen) channel.close()
synchronized(channels) { channels[handle] = null }
synchronized(channels) { channels.remove(handle) }
}
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
val channel = channels.getOrNull(handle)
val channel = channels[handle]
check(channel != null) { "Invalid logical channel handle $handle" }
if (runBlocking { verboseLoggingFlow.first() }) {

View file

@ -3,4 +3,5 @@
<locale android:name="en-US" />
<locale android:name="ja" />
<locale android:name="zh-CN" />
<locale android:name="zh-TW" />
</locale-config>

View file

@ -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))

View file

@ -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 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 getLaunchIntent(packageName: String) = try {
val pm = context.packageManager
pm.getLaunchIntentForPackage(packageName)
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)
)
}
val intent: Intent?
get() = getActivityIntent() ?: getDisabledPackageIntent()
}
}
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 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) }
private fun PackageManager.getActivities(packageName: String) = try {
getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
.activities?.toList() ?: emptyList()
} catch (_: PackageManager.NameNotFoundException) {
emptyList()
}
}
private fun getComponentNames(@ArrayRes id: Int) =
context.resources.getStringArray(id).mapNotNull(ComponentName::unflattenFromString)
fun isAvailable(slotId: Int) = when (slotId) {
-1 -> false
EuiccChannelManager.USB_CHANNEL_ID -> false
else -> intent(slotId) != null
}
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
}
}

View file

@ -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>