diff --git a/app-unpriv/src/main/AndroidManifest.xml b/app-unpriv/src/main/AndroidManifest.xml index 37cad5b..bce6831 100644 --- a/app-unpriv/src/main/AndroidManifest.xml +++ b/app-unpriv/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ android:theme="@style/Theme.OpenEUICC"> @@ -20,6 +20,11 @@ + + \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt new file mode 100644 index 0000000..20e0c5d --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/CompatibilityCheckActivity.kt @@ -0,0 +1,85 @@ +package im.angry.openeuicc.ui + +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import im.angry.easyeuicc.R +import im.angry.openeuicc.util.* +import kotlinx.coroutines.launch + +class CompatibilityCheckActivity: AppCompatActivity() { + private lateinit var compatibilityCheckList: RecyclerView + private val compatibilityChecks: List by lazy { getCompatibilityChecks(this) } + private val adapter = CompatibilityChecksAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_compatibility_check) + setSupportActionBar(findViewById(R.id.toolbar)) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + compatibilityCheckList = findViewById(R.id.recycler_view) + compatibilityCheckList.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) + compatibilityCheckList.adapter = adapter + } + + override fun onStart() { + super.onStart() + lifecycleScope.launch { + compatibilityChecks.executeAll { adapter.notifyDataSetChanged() } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + + inner class ViewHolder(private val root: View): RecyclerView.ViewHolder(root) { + private val titleView: TextView = root.findViewById(R.id.compatibility_check_title) + private val descView: TextView = root.findViewById(R.id.compatibility_check_desc) + + fun bindItem(item: CompatibilityCheck) { + titleView.text = item.title + descView.text = item.description + + when (item.state) { + CompatibilityCheck.State.SUCCESS -> { + root.findViewById(R.id.compatibility_check_checkmark).visibility = View.VISIBLE + } + CompatibilityCheck.State.FAILURE -> { + root.findViewById(R.id.compatibility_check_error).visibility = View.VISIBLE + } + else -> { + root.findViewById(R.id.compatibility_check_progress_bar).visibility = View.VISIBLE + } + } + } + } + + inner class CompatibilityChecksAdapter: RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + ViewHolder(layoutInflater.inflate(R.layout.compatibility_check_item, parent, false)) + + override fun getItemCount(): Int = + compatibilityChecks.indexOfLast { it.state != CompatibilityCheck.State.NOT_STARTED } + 1 + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bindItem(compatibilityChecks[position]) + } + } +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedMainActivity.kt b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedMainActivity.kt new file mode 100644 index 0000000..1ed0cce --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/ui/UnprivilegedMainActivity.kt @@ -0,0 +1,23 @@ +package im.angry.openeuicc.ui + +import android.content.Intent +import android.view.Menu +import android.view.MenuItem +import im.angry.easyeuicc.R + +class UnprivilegedMainActivity: MainActivity() { + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.activity_main_unprivileged, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + R.id.compatibility_check -> { + startActivity(Intent(this, CompatibilityCheckActivity::class.java)) + true + } + else -> super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt new file mode 100644 index 0000000..bccef18 --- /dev/null +++ b/app-unpriv/src/main/java/im/angry/openeuicc/util/CompatibilityCheck.kt @@ -0,0 +1,148 @@ +package im.angry.openeuicc.util + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.se.omapi.SEService +import android.telephony.TelephonyManager +import im.angry.easyeuicc.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +fun getCompatibilityChecks(context: Context): List = + listOf( + HasSystemFeaturesCheck(context), + OmapiConnCheck(context), + KnownBrokenCheck(context) + ) + +suspend fun List.executeAll(callback: () -> Unit) = withContext(Dispatchers.IO) { + forEach { + it.run() + withContext(Dispatchers.Main) { + callback() + } + } +} + +abstract class CompatibilityCheck(context: Context) { + enum class State { + NOT_STARTED, + IN_PROGRESS, + SUCCESS, + FAILURE + } + + var state = State.NOT_STARTED + + abstract val title: String + protected abstract val defaultDescription: String + protected lateinit var failureDescription: String + + val description: String + get() = when { + state == State.FAILURE && this::failureDescription.isInitialized -> failureDescription + else -> defaultDescription + } + + protected abstract suspend fun doCheck(): Boolean + + suspend fun run() { + state = State.IN_PROGRESS + delay(200) + state = try { + if (doCheck()) { + State.SUCCESS + } else { + State.FAILURE + } + } catch (_: Exception) { + State.FAILURE + } + } +} + +internal class HasSystemFeaturesCheck(private val context: Context): CompatibilityCheck(context) { + override val title: String + get() = context.getString(R.string.compatibility_check_system_features) + override val defaultDescription: String + get() = context.getString(R.string.compatibility_check_system_features_desc) + + override suspend fun doCheck(): Boolean { + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + failureDescription = context.getString(R.string.compatibility_check_system_features_no_telephony) + return false + } + + // We can check OMAPI UICC availability on R or later (if before R, we check OMAPI connectivity later) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !context.packageManager.hasSystemFeature( + PackageManager.FEATURE_SE_OMAPI_UICC + )) { + failureDescription = context.getString(R.string.compatibility_check_system_features_no_omapi) + return false + } + + return true + } +} + +internal class OmapiConnCheck(private val context: Context): CompatibilityCheck(context) { + override val title: String + get() = context.getString(R.string.compatibility_check_omapi_connectivity) + override val defaultDescription: String + get() = context.getString(R.string.compatibility_check_omapi_connectivity_desc) + + private suspend fun getSEService(): SEService = suspendCoroutine { cont -> + var service: SEService? = null + var resumed = false + val resume = { + if (!resumed && service != null) { + cont.resume(service!!) + resumed = true + } + } + service = SEService(context, { it.run() }, { resume() }) + Thread.sleep(1000) + resume() + } + + override suspend fun doCheck(): Boolean { + val seService = getSEService() + if (!seService.isConnected) { + failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail) + return false + } + + val tm = context.getSystemService(TelephonyManager::class.java) + val simReaders = seService.readers.filter { it.name.startsWith("SIM") } + if (simReaders.size < tm.activeModemCountCompat) { + failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail_sim_number, + simReaders.map { (it.name.replace("SIM", "").toIntOrNull() ?: 1) - 1 } + .joinToString(", ")) + return false + } + + return true + } +} + +internal class KnownBrokenCheck(private val context: Context): CompatibilityCheck(context) { + companion object { + val BROKEN_MANUFACTURERS = arrayOf("xiaomi") + } + + override val title: String + get() = context.getString(R.string.compatibility_check_known_broken) + override val defaultDescription: String + get() = context.getString(R.string.compatibility_check_known_broken_desc) + + init { + failureDescription = context.getString(R.string.compatibility_check_known_broken_fail) + } + + override suspend fun doCheck(): Boolean = + Build.MANUFACTURER.lowercase() !in BROKEN_MANUFACTURERS +} \ No newline at end of file diff --git a/app-unpriv/src/main/res/drawable/ic_checkmark_outline.xml b/app-unpriv/src/main/res/drawable/ic_checkmark_outline.xml new file mode 100644 index 0000000..c23123a --- /dev/null +++ b/app-unpriv/src/main/res/drawable/ic_checkmark_outline.xml @@ -0,0 +1,5 @@ + + + diff --git a/app-unpriv/src/main/res/drawable/ic_error_outline.xml b/app-unpriv/src/main/res/drawable/ic_error_outline.xml new file mode 100644 index 0000000..d265d6d --- /dev/null +++ b/app-unpriv/src/main/res/drawable/ic_error_outline.xml @@ -0,0 +1,5 @@ + + + diff --git a/app-unpriv/src/main/res/layout/activity_compatibility_check.xml b/app-unpriv/src/main/res/layout/activity_compatibility_check.xml new file mode 100644 index 0000000..2304c86 --- /dev/null +++ b/app-unpriv/src/main/res/layout/activity_compatibility_check.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app-unpriv/src/main/res/layout/compatibility_check_item.xml b/app-unpriv/src/main/res/layout/compatibility_check_item.xml new file mode 100644 index 0000000..f75741a --- /dev/null +++ b/app-unpriv/src/main/res/layout/compatibility_check_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-unpriv/src/main/res/menu/activity_main_unprivileged.xml b/app-unpriv/src/main/res/menu/activity_main_unprivileged.xml new file mode 100644 index 0000000..8542c04 --- /dev/null +++ b/app-unpriv/src/main/res/menu/activity_main_unprivileged.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app-unpriv/src/main/res/values/strings.xml b/app-unpriv/src/main/res/values/strings.xml index 06e1c54..7f43c4e 100644 --- a/app-unpriv/src/main/res/values/strings.xml +++ b/app-unpriv/src/main/res/values/strings.xml @@ -1,3 +1,17 @@ EasyEUICC + Compatibility Check + + + System Features + Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support. + Your device has no telephony features. + Your device has no support for accessing SIM cards via OMAPI. + OMAPI Connectivity + Does your device allow access to Secure Elements on SIM cards via OMAPI? + Unable to detect Secure Element readers for SIM cards via OMAPI. + Only the following SIM slots are accessible via OMAPI: %s. + Known Broken? + Making sure your device is not known to have bugs associated with removable eSIMs. + Oops, your device is known to have bugs when accessing removable eSIMs. This does not necessarily mean that it will not work at all, but you will have to proceed with caution. \ No newline at end of file