feat: Initial implementation: Compatibility Check
This commit is contained in:
parent
6dcaba1f11
commit
4a8648dada
|
@ -11,7 +11,7 @@
|
||||||
android:theme="@style/Theme.OpenEUICC">
|
android:theme="@style/Theme.OpenEUICC">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.MainActivity"
|
android:name="im.angry.openeuicc.ui.UnprivilegedMainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
@ -20,6 +20,11 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
||||||
|
android:label="@string/compatibility_check"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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>
|
5
app-unpriv/src/main/res/drawable/ic_error_outline.xml
Normal file
5
app-unpriv/src/main/res/drawable/ic_error_outline.xml
Normal 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>
|
|
@ -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>
|
65
app-unpriv/src/main/res/layout/compatibility_check_item.xml
Normal file
65
app-unpriv/src/main/res/layout/compatibility_check_item.xml
Normal 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>
|
|
@ -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>
|
|
@ -1,3 +1,17 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">EasyEUICC</string>
|
<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>
|
</resources>
|
Loading…
Reference in a new issue