feat: quick availability #196
12 changed files with 311 additions and 6 deletions
|
@ -23,6 +23,11 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="im.angry.openeuicc.ui.QuickAvailabilityActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/quick_availability" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.angry.openeuicc.di
|
package im.angry.openeuicc.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import im.angry.openeuicc.util.UnprivilegedPreferenceRepository
|
||||||
|
|
||||||
class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
||||||
override val uiComponentFactory by lazy {
|
override val uiComponentFactory by lazy {
|
||||||
|
@ -10,4 +11,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context)
|
||||||
override val customizableTextProvider by lazy {
|
override val customizableTextProvider by lazy {
|
||||||
UnprivilegedCustomizableTextProvider(context)
|
UnprivilegedCustomizableTextProvider(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val preferenceRepository by lazy {
|
||||||
|
UnprivilegedPreferenceRepository(context)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,12 +2,12 @@ package im.angry.openeuicc.di
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.angry.openeuicc.ui.EuiccManagementFragment
|
import im.angry.openeuicc.ui.EuiccManagementFragment
|
||||||
import im.angry.openeuicc.ui.SettingsFragment
|
import im.angry.openeuicc.ui.QuickAvailabilityFragment
|
||||||
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
|
import im.angry.openeuicc.ui.UnprivilegedEuiccManagementFragment
|
||||||
import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
|
import im.angry.openeuicc.ui.UnprivilegedNoEuiccPlaceholderFragment
|
||||||
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
|
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
|
||||||
|
|
||||||
class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||||
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
||||||
|
|
||||||
|
@ -16,4 +16,7 @@ class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||||
|
|
||||||
override fun createSettingsFragment(): Fragment =
|
override fun createSettingsFragment(): Fragment =
|
||||||
UnprivilegedSettingsFragment()
|
UnprivilegedSettingsFragment()
|
||||||
|
|
||||||
|
open fun createQuickAvailabilityFragment(): Fragment =
|
||||||
|
QuickAvailabilityFragment()
|
||||||
}
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.di.UnprivilegedUiComponentFactory
|
||||||
|
import im.angry.openeuicc.util.OpenEuiccContextMarker
|
||||||
|
|
||||||
|
class QuickAvailabilityActivity : AppCompatActivity(), OpenEuiccContextMarker {
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_FROM = "from_quick_availability"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContentView(R.layout.activity_quick_availability)
|
||||||
|
|
||||||
|
val quickAvailabilityFragment =
|
||||||
|
(appContainer.uiComponentFactory as UnprivilegedUiComponentFactory)
|
||||||
|
.createQuickAvailabilityFragment()
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.quick_availability_container, quickAvailabilityFragment)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchMainActivity() {
|
||||||
|
val intent = packageManager.getLaunchIntentForPackage(packageName)!!
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
intent.putExtra(EXTRA_FROM, true)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.icu.text.ListFormatter
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID
|
||||||
|
import im.angry.openeuicc.util.UnprivilegedEuiccContextMarker
|
||||||
|
import im.angry.openeuicc.util.connectSEService
|
||||||
|
import im.angry.openeuicc.util.decodeHex
|
||||||
|
import im.angry.openeuicc.util.isSIM
|
||||||
|
import im.angry.openeuicc.util.slotIndex
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
open class QuickAvailabilityFragment : Fragment(), UnprivilegedEuiccContextMarker {
|
||||||
|
companion object {
|
||||||
|
enum class Compatibility {
|
||||||
|
COMPATIBLE,
|
||||||
|
NOT_COMPATIBLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CompatibilityResult(
|
||||||
|
val compatibility: Compatibility,
|
||||||
|
val slots: List<String> = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val conclusion: TextView by lazy {
|
||||||
|
requireView().requireViewById(R.id.quick_availability_conclusion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val resultSlots: TextView by lazy {
|
||||||
|
requireView().requireViewById(R.id.quick_availability_result_slots)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val resultNotes: TextView by lazy {
|
||||||
|
requireView().requireViewById(R.id.quick_availability_result_notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hidden: CheckBox by lazy {
|
||||||
|
requireView().requireViewById(R.id.quick_availability_hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = inflater.inflate(R.layout.fragment_quick_availability, container, false).apply {
|
||||||
|
requireViewById<TextView>(R.id.quick_availability_device_information)
|
||||||
|
.text = formatDeviceInformation()
|
||||||
|
requireViewById<Button>(R.id.quick_availability_button_continue)
|
||||||
|
.setOnClickListener { onContinueToApp() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
onCompatibilityUpdate(getCompatibilityCheckResult())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onContinueToApp() {
|
||||||
|
runBlocking {
|
||||||
|
preferenceRepository.skipQuickAvailabilityFlow
|
||||||
|
.updatePreference(hidden.isChecked)
|
||||||
|
}
|
||||||
|
(requireActivity() as QuickAvailabilityActivity).launchMainActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCompatibilityUpdate(result: CompatibilityResult) {
|
||||||
|
conclusion.text = formatConclusion(result)
|
||||||
|
if (result.compatibility != Compatibility.COMPATIBLE) return
|
||||||
|
resultSlots.isVisible = true
|
||||||
|
resultSlots.text = getString(
|
||||||
|
R.string.quick_availability_result_slots,
|
||||||
|
ListFormatter.getInstance().format(result.slots)
|
||||||
|
)
|
||||||
|
resultNotes.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCompatibilityCheckResult(): CompatibilityResult {
|
||||||
|
val service = connectSEService(requireContext())
|
||||||
|
if (!service.isConnected) {
|
||||||
|
return CompatibilityResult(Compatibility.NOT_COMPATIBLE)
|
||||||
|
}
|
||||||
|
val slots = service.readers.filter { it.isSIM }.mapNotNull { reader ->
|
||||||
|
try {
|
||||||
|
// Note: we ONLY check the default ISD-R AID, because this test is for the _device_,
|
||||||
|
// NOT the eUICC. We don't care what AID a potential eUICC might use, all we need to
|
||||||
|
// check is we can open _some_ AID.
|
||||||
|
reader.openSession().openLogicalChannel(EUICC_DEFAULT_ISDR_AID.decodeHex())?.close()
|
||||||
|
reader.slotIndex
|
||||||
|
} catch (_: SecurityException) {
|
||||||
|
// Ignore; this is expected when everything works
|
||||||
|
// ref: https://android.googlesource.com/platform/frameworks/base/+/4fe64fb4712a99d5da9c9a0eb8fd5169b252e1e1/omapi/java/android/se/omapi/Session.java#305
|
||||||
|
// SecurityException is only thrown when Channel is constructed, which means everything else needs to succeed
|
||||||
|
reader.slotIndex
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slots.isEmpty()) {
|
||||||
|
return CompatibilityResult(Compatibility.NOT_COMPATIBLE)
|
||||||
|
}
|
||||||
|
return CompatibilityResult(Compatibility.COMPATIBLE, slots = slots.map { "SIM$it" })
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun formatConclusion(result: CompatibilityResult): String {
|
||||||
|
val usbHost = requireContext().packageManager
|
||||||
|
.hasSystemFeature(PackageManager.FEATURE_USB_HOST)
|
||||||
|
val resId = when (result.compatibility) {
|
||||||
|
Compatibility.COMPATIBLE ->
|
||||||
|
R.string.quick_availability_compatible
|
||||||
|
|
||||||
|
Compatibility.NOT_COMPATIBLE -> if (usbHost)
|
||||||
|
R.string.quick_availability_not_compatible_but_usb else
|
||||||
|
R.string.quick_availability_not_compatible
|
||||||
|
}
|
||||||
|
return getString(resId, getString(R.string.app_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun formatDeviceInformation() = buildString {
|
||||||
|
appendLine("BRAND: ${Build.BRAND}")
|
||||||
|
appendLine("DEVICE: ${Build.DEVICE}")
|
||||||
|
appendLine("MODEL: ${Build.MODEL}")
|
||||||
|
appendLine("VERSION.RELEASE: ${Build.VERSION.RELEASE}")
|
||||||
|
appendLine("VERSION.SDK_INT: ${Build.VERSION.SDK_INT}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,27 @@
|
||||||
package im.angry.openeuicc.ui
|
package im.angry.openeuicc.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import im.angry.easyeuicc.R
|
import im.angry.easyeuicc.R
|
||||||
|
import im.angry.openeuicc.di.UnprivilegedUiComponentFactory
|
||||||
|
import im.angry.openeuicc.util.UnprivilegedEuiccContextMarker
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class UnprivilegedMainActivity : MainActivity(), UnprivilegedEuiccContextMarker {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (intent.getBooleanExtra(QuickAvailabilityActivity.EXTRA_FROM, false)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (runBlocking { !preferenceRepository.skipQuickAvailabilityFlow.first() }) {
|
||||||
|
startActivity(Intent(this, QuickAvailabilityActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class UnprivilegedMainActivity: MainActivity() {
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
menuInflater.inflate(R.menu.activity_main_unprivileged, menu)
|
menuInflater.inflate(R.menu.activity_main_unprivileged, menu)
|
||||||
|
|
|
@ -33,10 +33,10 @@ suspend fun List<CompatibilityCheck>.executeAll(callback: () -> Unit) = withCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Reader.isSIM: Boolean
|
val Reader.isSIM: Boolean
|
||||||
get() = name.startsWith("SIM")
|
get() = name.startsWith("SIM")
|
||||||
|
|
||||||
private val Reader.slotIndex: Int
|
val Reader.slotIndex: Int
|
||||||
get() = (name.replace("SIM", "").toIntOrNull() ?: 1)
|
get() = (name.replace("SIM", "").toIntOrNull() ?: 1)
|
||||||
|
|
||||||
abstract class CompatibilityCheck(context: Context) {
|
abstract class CompatibilityCheck(context: Context) {
|
||||||
|
@ -173,7 +173,7 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result != State.SUCCESS && validSlotIds.size > 0) {
|
if (result != State.SUCCESS && validSlotIds.isNotEmpty()) {
|
||||||
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
|
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
|
||||||
failureDescription = context.getString(
|
failureDescription = context.getString(
|
||||||
R.string.compatibility_check_isdr_channel_desc_partial_fail,
|
R.string.compatibility_check_isdr_channel_desc_partial_fail,
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
|
||||||
|
internal object UnprivilegedPreferenceKeys {
|
||||||
|
// ---- Miscellaneous ----
|
||||||
|
val SKIP_QUICK_AVAILABILITY = booleanPreferencesKey("skip_quick_availability")
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnprivilegedPreferenceRepository(context: Context) : PreferenceRepository(context) {
|
||||||
|
// ---- Miscellaneous ----
|
||||||
|
val skipQuickAvailabilityFlow = bindFlow(UnprivilegedPreferenceKeys.SKIP_QUICK_AVAILABILITY, false)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package im.angry.openeuicc.util
|
||||||
|
|
||||||
|
interface UnprivilegedEuiccContextMarker : OpenEuiccContextMarker {
|
||||||
|
override val preferenceRepository: UnprivilegedPreferenceRepository
|
||||||
|
get() = appContainer.preferenceRepository as UnprivilegedPreferenceRepository
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/quick_availability_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:textAlignment="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/quick_availability_conclusion"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/quick_availability_device_information"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineHeight="30dp"
|
||||||
|
android:textAlignment="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/quick_availability_result_slots"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/quick_availability_result_notes"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/quick_availability_result_notes"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/quick_availability_hidden"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/quick_availability_hidden" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/quick_availability_button_continue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/quick_availability_button_continue" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -11,6 +11,16 @@
|
||||||
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
|
<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>
|
<string name="toast_prompt_to_enable_sim_toolkit">Please ENABLE your \"%s\" application</string>
|
||||||
|
|
||||||
|
<!-- Quick Availability -->
|
||||||
|
<string name="quick_availability">Quick Availability</string>
|
||||||
|
<string name="quick_availability_compatible">Your smartphone can use %s compatible cards</string>
|
||||||
|
<string name="quick_availability_not_compatible">Your smartphone is not compatible with %s</string>
|
||||||
|
<string name="quick_availability_not_compatible_but_usb">Your smartphone is not compatible with %s, but can be managed using a USB smart card reader</string>
|
||||||
|
<string name="quick_availability_result_slots">SIM card slots accessible: %s</string>
|
||||||
|
<string name="quick_availability_result_notes">Note: Quick Compatibility Check results are for reference only. Actual usage may vary based on card insertion.</string>
|
||||||
|
<string name="quick_availability_hidden">Do not show this message again.</string>
|
||||||
|
<string name="quick_availability_button_continue">Continue</string>
|
||||||
|
|
||||||
<!-- Compatibility Check Descriptions -->
|
<!-- Compatibility Check Descriptions -->
|
||||||
<string name="compatibility_check_system_features">System Features</string>
|
<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_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue