feat: Initial implementation: Compatibility Check

This commit is contained in:
Peter Cai 2024-01-28 10:21:20 -05:00
parent 6dcaba1f11
commit 4a8648dada
10 changed files with 383 additions and 1 deletions

View file

@ -11,7 +11,7 @@
android:theme="@style/Theme.OpenEUICC">
<activity
android:name="im.angry.openeuicc.ui.MainActivity"
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -20,6 +20,11 @@
</intent-filter>
</activity>
<activity
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
android:label="@string/compatibility_check"
android:exported="false" />
</application>
</manifest>

View file

@ -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<CompatibilityCheck> 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<View>(R.id.compatibility_check_checkmark).visibility = View.VISIBLE
}
CompatibilityCheck.State.FAILURE -> {
root.findViewById<View>(R.id.compatibility_check_error).visibility = View.VISIBLE
}
else -> {
root.findViewById<View>(R.id.compatibility_check_progress_bar).visibility = View.VISIBLE
}
}
}
}
inner class CompatibilityChecksAdapter: RecyclerView.Adapter<ViewHolder>() {
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])
}
}
}

View file

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

View file

@ -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<CompatibilityCheck> =
listOf(
HasSystemFeaturesCheck(context),
OmapiConnCheck(context),
KnownBrokenCheck(context)
)
suspend fun List<CompatibilityCheck>.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
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16.59,7.58L10,14.17l-3.59,-3.58L5,12l5,5 8,-8zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/compatibility_check_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="12dp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/compatibility_check_status_container"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/compatibility_check_desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/compatibility_check_status_container"
app:layout_constraintTop_toBottomOf="@id/compatibility_check_title"
app:layout_constraintBottom_toBottomOf="parent" />
<FrameLayout
android:id="@+id/compatibility_check_status_container"
android:layout_width="32dp"
android:layout_height="match_parent"
android:layout_marginEnd="24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ProgressBar
android:id="@+id/compatibility_check_progress_bar"
android:visibility="gone"
android:indeterminate="true"
android:layout_gravity="center"
android:layout_width="32dp"
android:layout_height="32dp" />
<ImageView
android:id="@+id/compatibility_check_checkmark"
android:src="@drawable/ic_checkmark_outline"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="32dp"
android:layout_height="32dp" />
<ImageView
android:id="@+id/compatibility_check_error"
android:src="@drawable/ic_error_outline"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="32dp"
android:layout_height="32dp" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/compatibility_check"
android:title="@string/compatibility_check"
app:showAsAction="never" />
</menu>

View file

@ -1,3 +1,17 @@
<resources>
<string name="app_name" translatable="false">EasyEUICC</string>
<string name="compatibility_check">Compatibility Check</string>
<!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string>
<string name="compatibility_check_system_features_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string>
<string name="compatibility_check_system_features_no_telephony">Your device has no telephony features.</string>
<string name="compatibility_check_system_features_no_omapi">Your device has no support for accessing SIM cards via OMAPI.</string>
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI.</string>
<string name="compatibility_check_omapi_connectivity_fail_sim_number">Only the following SIM slots are accessible via OMAPI: %s.</string>
<string name="compatibility_check_known_broken">Known Broken?</string>
<string name="compatibility_check_known_broken_desc">Making sure your device is not known to have bugs associated with removable eSIMs.</string>
<string name="compatibility_check_known_broken_fail">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.</string>
</resources>