feat: quick availability #196
12 changed files with 311 additions and 6 deletions
|
@ -23,6 +23,11 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.QuickAvailabilityActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/quick_availability" />
|
||||
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.CompatibilityCheckActivity"
|
||||
android:exported="false"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.di
|
||||
|
||||
import android.content.Context
|
||||
import im.angry.openeuicc.util.UnprivilegedPreferenceRepository
|
||||
|
||||
class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
|
||||
override val uiComponentFactory by lazy {
|
||||
|
@ -10,4 +11,8 @@ class UnprivilegedAppContainer(context: Context) : DefaultAppContainer(context)
|
|||
override val customizableTextProvider by lazy {
|
||||
UnprivilegedCustomizableTextProvider(context)
|
||||
}
|
||||
|
||||
override val preferenceRepository by lazy {
|
||||
UnprivilegedPreferenceRepository(context)
|
||||
}
|
||||
}
|
|
@ -2,12 +2,12 @@ package im.angry.openeuicc.di
|
|||
|
||||
import androidx.fragment.app.Fragment
|
||||
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.UnprivilegedNoEuiccPlaceholderFragment
|
||||
import im.angry.openeuicc.ui.UnprivilegedSettingsFragment
|
||||
|
||||
class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||
open class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
||||
override fun createEuiccManagementFragment(slotId: Int, portId: Int): EuiccManagementFragment =
|
||||
UnprivilegedEuiccManagementFragment.newInstance(slotId, portId)
|
||||
|
||||
|
@ -16,4 +16,7 @@ class UnprivilegedUiComponentFactory : DefaultUiComponentFactory() {
|
|||
|
||||
override fun createSettingsFragment(): Fragment =
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
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 {
|
||||
super.onCreateOptionsMenu(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")
|
||||
|
||||
private val Reader.slotIndex: Int
|
||||
val Reader.slotIndex: Int
|
||||
get() = (name.replace("SIM", "").toIntOrNull() ?: 1)
|
||||
|
||||
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)) {
|
||||
failureDescription = context.getString(
|
||||
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_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 -->
|
||||
<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>
|
||||
|
|
Loading…
Add table
Reference in a new issue