From b9d5c1c5bbd02bbfa35eb5d7d914c57f35746760 Mon Sep 17 00:00:00 2001 From: septs Date: Mon, 18 Nov 2024 23:39:09 +0100 Subject: [PATCH 01/33] chore: simplify dot-idea gitignore (#68) Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/68 Co-authored-by: septs Co-committed-by: septs --- .gitignore | 7 ------- .idea/.gitignore | 9 ++++++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 2b15a47..6cc80a0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,6 @@ .gradle /local.properties /keystore.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -/.idea/deploymentTargetDropDown.xml .DS_Store /build /captures diff --git a/.idea/.gitignore b/.idea/.gitignore index 26d3352..c9924ab 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,10 @@ # Default ignored files -/shelf/ +/shelf +/caches +/libraries +/modules.xml /workspace.xml +/navEditor.xml +/assetWizardSettings.xml +/deploymentTargetDropDown.xml +/runConfigurations.xml \ No newline at end of file From 98e16ee5aad9a051383e1f894600b381f92f4570 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 18 Nov 2024 19:57:01 -0500 Subject: [PATCH 02/33] ui: Hook up prev / next buttons for new download wizard --- .../ui/wizard/DownloadWizardActivity.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 408c55d..f3cb114 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -45,6 +45,28 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { nextButton = requireViewById(R.id.download_wizard_next) prevButton = requireViewById(R.id.download_wizard_back) + nextButton.setOnClickListener { + if (currentFragment?.hasNext == true) { + val nextFrag = currentFragment?.createNextFragment() + if (nextFrag == null) { + finish() + } else { + showFragment(nextFrag) + } + } + } + + prevButton.setOnClickListener { + if (currentFragment?.hasPrev == true) { + val prevFrag = currentFragment?.createPrevFragment() + if (prevFrag == null) { + finish() + } else { + showFragment(prevFrag) + } + } + } + val navigation = requireViewById(R.id.download_wizard_navigation) val origHeight = navigation.layoutParams.height From aaca9e807a320f943331104c8bf811bfcac031f8 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 18 Nov 2024 20:01:43 -0500 Subject: [PATCH 03/33] ui: Show free space when selecting slot --- .../wizard/DownloadWizardSlotSelectFragment.kt | 4 ++++ .../src/main/res/layout/download_slot_item.xml | 16 +++++++++++++++- app-common/src/main/res/values/strings.xml | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 6720242..059f856 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -26,6 +26,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt val hasMultiplePorts: Boolean, val portId: Int, val eID: String, + val freeSpace: Int, val enabledProfileName: String? ) @@ -77,6 +78,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt channel.port.card.ports.size > 1, channel.portId, channel.lpa.eID, + channel.lpa.euiccInfo2?.freeNvram ?: 0, channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName ) } @@ -105,6 +107,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt private val type = root.requireViewById(R.id.slot_item_type) private val eID = root.requireViewById(R.id.slot_item_eid) private val activeProfile = root.requireViewById(R.id.slot_item_active_profile) + private val freeSpace = root.requireViewById(R.id.slot_item_free_space) private val checkBox = root.requireViewById(R.id.slot_checkbox) private var curIdx = -1 @@ -143,6 +146,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt title.text = root.context.getString(R.string.download_wizard_slot_title, item.logicalSlotId) eID.text = item.eID activeProfile.text = item.enabledProfileName ?: root.context.getString(R.string.unknown) + freeSpace.text = formatFreeSpace(item.freeSpace) checkBox.isChecked = adapter.currentSelectedIdx == idx } } diff --git a/app-common/src/main/res/layout/download_slot_item.xml b/app-common/src/main/res/layout/download_slot_item.xml index fa06b4c..d0ca176 100644 --- a/app-common/src/main/res/layout/download_slot_item.xml +++ b/app-common/src/main/res/layout/download_slot_item.xml @@ -61,6 +61,20 @@ android:layout_height="wrap_content" android:textSize="14sp" /> + + + + Internal, port %d eID: Active Profile: + Free Space: New nickname From 0c519af376b54207f3163b879089820a3cb97fa9 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 18 Nov 2024 20:19:28 -0500 Subject: [PATCH 04/33] ui: Update slot select prompt text --- .../src/main/res/layout/fragment_download_slot_select.xml | 7 ++++++- app-common/src/main/res/values/strings.xml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app-common/src/main/res/layout/fragment_download_slot_select.xml b/app-common/src/main/res/layout/fragment_download_slot_select.xml index 6bd2e5d..3dfe6fd 100644 --- a/app-common/src/main/res/layout/fragment_download_slot_select.xml +++ b/app-common/src/main/res/layout/fragment_download_slot_select.xml @@ -9,10 +9,15 @@ android:text="@string/download_wizard_slot_select" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:gravity="center_horizontal" android:textSize="20sp" - android:layout_margin="20sp" + android:layout_marginTop="20sp" + android:layout_marginBottom="20sp" + android:layout_marginStart="60sp" + android:layout_marginEnd="60sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constrainedWidth="true" app:layout_constraintTop_toTopOf="parent" /> Download Wizard Back Next - Confirm the eSIM slot: + Select or confirm the eSIM you would like to download to: Logical slot %d Type: Removable From 92b7b46598b0881f7d973d0077164f8b9f5827fc Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 18 Nov 2024 20:54:42 -0500 Subject: [PATCH 05/33] ui: Lay out the method select fragment for wizard --- .../DownloadWizardMethodSelectFragment.kt | 93 +++++++++++++++++++ .../DownloadWizardSlotSelectFragment.kt | 5 +- app-common/src/main/res/drawable/ic_edit.xml | 5 + .../main/res/layout/download_method_item.xml | 44 +++++++++ .../fragment_download_method_select.xml | 33 +++++++ app-common/src/main/res/values/strings.xml | 4 + 6 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt create mode 100644 app-common/src/main/res/drawable/ic_edit.xml create mode 100644 app-common/src/main/res/layout/download_method_item.xml create mode 100644 app-common/src/main/res/layout/fragment_download_method_select.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt new file mode 100644 index 0000000..51cf679 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -0,0 +1,93 @@ +package im.angry.openeuicc.ui.wizard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import im.angry.openeuicc.common.R + +class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + data class DownloadMethod( + val iconRes: Int, + val titleRes: Int, + val onClick: () -> Unit + ) + + val downloadMethods = arrayOf( + DownloadMethod(R.drawable.ic_scan_black, R.string.download_wizard_method_qr_code) { + + }, + DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) { + + }, + DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) { + + } + ) + + override val hasNext: Boolean + get() = false + override val hasPrev: Boolean + get() = true + + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? { + TODO("Not yet implemented") + } + + override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = + DownloadWizardSlotSelectFragment() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_download_method_select, container, false) + val recyclerView = view.requireViewById(R.id.download_method_list) + recyclerView.adapter = DownloadMethodAdapter() + recyclerView.layoutManager = + LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) + recyclerView.addItemDecoration( + DividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) + ) + return view + } + + private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) { + private val icon = root.requireViewById(R.id.download_method_icon) + private val title = root.requireViewById(R.id.download_method_title) + + fun bind(item: DownloadMethod) { + icon.setImageResource(item.iconRes) + title.setText(item.titleRes) + root.setOnClickListener { item.onClick() } + } + } + + private inner class DownloadMethodAdapter : RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): DownloadMethodViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.download_method_item, parent, false) + return DownloadMethodViewHolder(view) + } + + override fun getItemCount(): Int = downloadMethods.size + + override fun onBindViewHolder(holder: DownloadMethodViewHolder, position: Int) { + holder.bind(downloadMethods[position]) + } + + } +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 059f856..3458227 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -39,9 +39,8 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt override val hasPrev: Boolean get() = true - override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? { - TODO("Not yet implemented") - } + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment = + DownloadWizardMethodSelectFragment() override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null diff --git a/app-common/src/main/res/drawable/ic_edit.xml b/app-common/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..3c53db7 --- /dev/null +++ b/app-common/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app-common/src/main/res/layout/download_method_item.xml b/app-common/src/main/res/layout/download_method_item.xml new file mode 100644 index 0000000..5b2c2a8 --- /dev/null +++ b/app-common/src/main/res/layout/download_method_item.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/layout/fragment_download_method_select.xml b/app-common/src/main/res/layout/fragment_download_method_select.xml new file mode 100644 index 0000000..a57e186 --- /dev/null +++ b/app-common/src/main/res/layout/fragment_download_method_select.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 5a88fe8..bc48142 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -70,6 +70,10 @@ eID: Active Profile: Free Space: + How would you like to download the eSIM profile? + Scan a QR code with camera + Load a QR code from gallery + Enter manually New nickname From dbdadd33b3e7bc1bb39140eee8b5895989ab035b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 18 Nov 2024 21:02:12 -0500 Subject: [PATCH 06/33] ui: Add slide-in and slide-out animation for wizard steps --- .../openeuicc/ui/wizard/DownloadWizardActivity.kt | 13 +++++++++---- app-common/src/main/res/anim/slide_in_left.xml | 6 ++++++ app-common/src/main/res/anim/slide_in_right.xml | 6 ++++++ app-common/src/main/res/anim/slide_out_left.xml | 6 ++++++ app-common/src/main/res/anim/slide_out_right.xml | 6 ++++++ 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 app-common/src/main/res/anim/slide_in_left.xml create mode 100644 app-common/src/main/res/anim/slide_in_right.xml create mode 100644 app-common/src/main/res/anim/slide_out_left.xml create mode 100644 app-common/src/main/res/anim/slide_out_right.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index f3cb114..09dd38c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -51,7 +51,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { if (nextFrag == null) { finish() } else { - showFragment(nextFrag) + showFragment(nextFrag, R.anim.slide_in_right, R.anim.slide_out_left) } } } @@ -62,7 +62,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { if (prevFrag == null) { finish() } else { - showFragment(prevFrag) + showFragment(prevFrag, R.anim.slide_in_left, R.anim.slide_out_right) } } } @@ -98,9 +98,14 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { showFragment(DownloadWizardSlotSelectFragment()) } - private fun showFragment(nextFrag: DownloadWizardStepFragment) { + private fun showFragment( + nextFrag: DownloadWizardStepFragment, + enterAnim: Int = 0, + exitAnim: Int = 0 + ) { currentFragment = nextFrag - supportFragmentManager.beginTransaction().replace(R.id.step_fragment_container, nextFrag) + supportFragmentManager.beginTransaction().setCustomAnimations(enterAnim, exitAnim) + .replace(R.id.step_fragment_container, nextFrag) .commit() refreshButtons() } diff --git a/app-common/src/main/res/anim/slide_in_left.xml b/app-common/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..9078d1f --- /dev/null +++ b/app-common/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,6 @@ + + diff --git a/app-common/src/main/res/anim/slide_in_right.xml b/app-common/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..42aa3f5 --- /dev/null +++ b/app-common/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,6 @@ + + diff --git a/app-common/src/main/res/anim/slide_out_left.xml b/app-common/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..1a806a9 --- /dev/null +++ b/app-common/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,6 @@ + + diff --git a/app-common/src/main/res/anim/slide_out_right.xml b/app-common/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..f209f38 --- /dev/null +++ b/app-common/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,6 @@ + + From 723ec70730c9102066410407281b66856ef11f45 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Mon, 18 Nov 2024 21:06:46 -0500 Subject: [PATCH 07/33] ui: Use prev button action for back pressed --- .../ui/wizard/DownloadWizardActivity.kt | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 09dd38c..8aab4e3 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -33,7 +33,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { setContentView(R.layout.activity_download_wizard) onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - // TODO: Actually implement this + // Make back == prev + onPrevPressed() } }) @@ -46,25 +47,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { prevButton = requireViewById(R.id.download_wizard_back) nextButton.setOnClickListener { - if (currentFragment?.hasNext == true) { - val nextFrag = currentFragment?.createNextFragment() - if (nextFrag == null) { - finish() - } else { - showFragment(nextFrag, R.anim.slide_in_right, R.anim.slide_out_left) - } - } + onNextPressed() } prevButton.setOnClickListener { - if (currentFragment?.hasPrev == true) { - val prevFrag = currentFragment?.createPrevFragment() - if (prevFrag == null) { - finish() - } else { - showFragment(prevFrag, R.anim.slide_in_left, R.anim.slide_out_right) - } - } + onPrevPressed() } val navigation = requireViewById(R.id.download_wizard_navigation) @@ -93,6 +80,28 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { } } + private fun onPrevPressed() { + if (currentFragment?.hasPrev == true) { + val prevFrag = currentFragment?.createPrevFragment() + if (prevFrag == null) { + finish() + } else { + showFragment(prevFrag, R.anim.slide_in_left, R.anim.slide_out_right) + } + } + } + + private fun onNextPressed() { + if (currentFragment?.hasNext == true) { + val nextFrag = currentFragment?.createNextFragment() + if (nextFrag == null) { + finish() + } else { + showFragment(nextFrag, R.anim.slide_in_right, R.anim.slide_out_left) + } + } + } + override fun onInit() { progressBar.visibility = View.GONE showFragment(DownloadWizardSlotSelectFragment()) From 9cf95ad47ce540383a7f5040714f4f0f19958e76 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 19 Nov 2024 18:38:59 -0500 Subject: [PATCH 08/33] ui: Add a input details fragment for download wizard --- .../ui/wizard/DownloadWizardActivity.kt | 9 ++ .../wizard/DownloadWizardDetailsFragment.kt | 30 ++++++ .../DownloadWizardMethodSelectFragment.kt | 2 +- .../res/layout/fragment_download_details.xml | 97 +++++++++++++++++++ app-common/src/main/res/values/strings.xml | 1 + 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt create mode 100644 app-common/src/main/res/layout/fragment_download_details.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 8aab4e3..9387c77 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -143,6 +143,15 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { abstract fun createNextFragment(): DownloadWizardStepFragment? abstract fun createPrevFragment(): DownloadWizardStepFragment? + protected fun gotoNextFragment(next: DownloadWizardStepFragment? = null) { + val realNext = next ?: createNextFragment() + (requireActivity() as DownloadWizardActivity).showFragment( + realNext!!, + R.anim.slide_in_right, + R.anim.slide_out_left + ) + } + protected fun hideProgressBar() { (requireActivity() as DownloadWizardActivity).progressBar.visibility = View.GONE } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt new file mode 100644 index 0000000..fda9d31 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -0,0 +1,30 @@ +package im.angry.openeuicc.ui.wizard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import im.angry.openeuicc.common.R + +class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + override val hasNext: Boolean + get() = false + override val hasPrev: Boolean + get() = true + + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? { + TODO("Not yet implemented") + } + + override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = + DownloadWizardMethodSelectFragment() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_download_details, container, false) + return view + } +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index 51cf679..1b813a4 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -27,7 +27,7 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard }, DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) { - + gotoNextFragment(DownloadWizardDetailsFragment()) } ) diff --git a/app-common/src/main/res/layout/fragment_download_details.xml b/app-common/src/main/res/layout/fragment_download_details.xml new file mode 100644 index 0000000..0820c27 --- /dev/null +++ b/app-common/src/main/res/layout/fragment_download_details.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index bc48142..3b075bb 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -74,6 +74,7 @@ Scan a QR code with camera Load a QR code from gallery Enter manually + Input or confirm details for downloading your eSIM: New nickname From 8c73615fbb49638bc351e62020e0e0b68c03ff11 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 19 Nov 2024 20:11:37 -0500 Subject: [PATCH 09/33] ui: wizard: Implement input by scanning / gallery --- .../ui/wizard/DownloadWizardActivity.kt | 12 +++- .../wizard/DownloadWizardDetailsFragment.kt | 33 ++++++++++- .../DownloadWizardMethodSelectFragment.kt | 58 +++++++++++++++++-- .../DownloadWizardSlotSelectFragment.kt | 13 ++++- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 9387c77..a004862 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -16,7 +16,11 @@ import im.angry.openeuicc.util.* class DownloadWizardActivity: BaseEuiccAccessActivity() { data class DownloadWizardState( - var selectedLogicalSlot: Int + var selectedLogicalSlot: Int, + var smdp: String, + var matchingId: String, + var confirmationCode: String, + var imei: String, ) private lateinit var state: DownloadWizardState @@ -39,7 +43,11 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { }) state = DownloadWizardState( - intent.getIntExtra("selectedLogicalSlot", 0) + intent.getIntExtra("selectedLogicalSlot", 0), + "", + "", + "", + "" ) progressBar = requireViewById(R.id.progress) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index fda9d31..c4a484f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -1,17 +1,27 @@ package im.angry.openeuicc.ui.wizard import android.os.Bundle +import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.widget.addTextChangedListener +import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + private var inputComplete = false + override val hasNext: Boolean - get() = false + get() = inputComplete override val hasPrev: Boolean get() = true + private lateinit var smdp: TextInputLayout + private lateinit var matchingId: TextInputLayout + private lateinit var confirmationCode: TextInputLayout + private lateinit var imei: TextInputLayout + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? { TODO("Not yet implemented") } @@ -25,6 +35,27 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_download_details, container, false) + smdp = view.requireViewById(R.id.profile_download_server) + matchingId = view.requireViewById(R.id.profile_download_code) + confirmationCode = view.requireViewById(R.id.profile_download_confirmation_code) + imei = view.requireViewById(R.id.profile_download_imei) + smdp.editText!!.addTextChangedListener { + updateInputCompleteness() + } return view } + + override fun onStart() { + super.onStart() + smdp.editText!!.setText(state.smdp) + matchingId.editText!!.setText(state.matchingId) + confirmationCode.editText!!.setText(state.confirmationCode) + imei.editText!!.setText(state.imei) + updateInputCompleteness() + } + + private fun updateInputCompleteness() { + inputComplete = Patterns.DOMAIN_NAME.matcher(smdp.editText!!.text).matches() + refreshButtons() + } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index 1b813a4..4d8a38f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -1,16 +1,25 @@ package im.angry.openeuicc.ui.wizard +import android.graphics.BitmapFactory import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions import im.angry.openeuicc.common.R +import im.angry.openeuicc.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizardStepFragment() { data class DownloadMethod( @@ -19,12 +28,44 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard val onClick: () -> Unit ) + // TODO: Maybe we should find a better barcode scanner (or an external one?) + private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> + result.contents?.let { content -> + processLpaString(content) + } + } + + private val gallerySelectorLauncher = + registerForActivityResult(ActivityResultContracts.GetContent()) { result -> + if (result == null) return@registerForActivityResult + + lifecycleScope.launch(Dispatchers.IO) { + runCatching { + requireContext().contentResolver.openInputStream(result)?.let { input -> + val bmp = BitmapFactory.decodeStream(input) + input.close() + + decodeQrFromBitmap(bmp)?.let { + withContext(Dispatchers.Main) { + processLpaString(it) + } + } + + bmp.recycle() + } + } + } + } + val downloadMethods = arrayOf( DownloadMethod(R.drawable.ic_scan_black, R.string.download_wizard_method_qr_code) { - + barcodeScannerLauncher.launch(ScanOptions().apply { + setDesiredBarcodeFormats(ScanOptions.QR_CODE) + setOrientationLocked(false) + }) }, DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) { - + gallerySelectorLauncher.launch("image/*") }, DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) { gotoNextFragment(DownloadWizardDetailsFragment()) @@ -36,9 +77,8 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard override val hasPrev: Boolean get() = true - override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? { - TODO("Not yet implemented") - } + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = + null override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = DownloadWizardSlotSelectFragment() @@ -62,6 +102,14 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard return view } + private fun processLpaString(s: String) { + val components = s.split("$") + if (components.size < 3 || components[0] != "LPA:1") return + state.smdp = components[1] + state.matchingId = components[2] + gotoNextFragment(DownloadWizardDetailsFragment()) + } + private class DownloadMethodViewHolder(private val root: View) : ViewHolder(root) { private val icon = root.requireViewById(R.id.download_method_icon) private val title = root.requireViewById(R.id.download_method_title) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 3458227..2217270 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -27,6 +27,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt val portId: Int, val eID: String, val freeSpace: Int, + val imei: String, val enabledProfileName: String? ) @@ -65,7 +66,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt } } - @SuppressLint("NotifyDataSetChanged") + @SuppressLint("NotifyDataSetChanged", "MissingPermission") private suspend fun init() { ensureEuiccChannelManager() showProgressBar(-1) @@ -78,6 +79,11 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt channel.portId, channel.lpa.eID, channel.lpa.euiccInfo2?.freeNvram ?: 0, + try { + telephonyManager.getImei(channel.logicalSlotId) ?: "" + } catch (e: Exception) { + "" + }, channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName ) } @@ -95,6 +101,10 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt 0 } + if (slots.isNotEmpty()) { + state.imei = slots[adapter.currentSelectedIdx].imei + } + adapter.notifyDataSetChanged() hideProgressBar() loaded = true @@ -126,6 +136,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt adapter.notifyItemChanged(curIdx) // Selected index isn't logical slot ID directly, needs a conversion state.selectedLogicalSlot = adapter.slots[adapter.currentSelectedIdx].logicalSlotId + state.imei = adapter.slots[adapter.currentSelectedIdx].imei } fun bind(item: SlotInfo, idx: Int) { From 81f34f9b1c310109d02b2608d8741134c5372164 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 19 Nov 2024 20:14:30 -0500 Subject: [PATCH 10/33] ui: wizard: Sort by slot ID --- .../openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index 2217270..c9a9e0f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -87,7 +87,7 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt channel.lpa.profiles.find { it.state == LocalProfileInfo.State.Enabled }?.displayName ) } - }.toList() + }.toList().sortedBy { it.logicalSlotId } adapter.slots = slots // Ensure we always have a selected slot by default From e7a04822811806cc473a0d12636dd0c45226964c Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 19 Nov 2024 20:40:53 -0500 Subject: [PATCH 11/33] ui: wizard: Save current state to bundle --- .../ui/wizard/DownloadWizardActivity.kt | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index a004862..029725c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -16,6 +16,7 @@ import im.angry.openeuicc.util.* class DownloadWizardActivity: BaseEuiccAccessActivity() { data class DownloadWizardState( + var currentStepFragmentClassName: String?, var selectedLogicalSlot: Int, var smdp: String, var matchingId: String, @@ -30,6 +31,12 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { private lateinit var prevButton: Button private var currentFragment: DownloadWizardStepFragment? = null + set(value) { + if (this::state.isInitialized) { + state.currentStepFragmentClassName = value?.javaClass?.name + } + field = value + } override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -43,6 +50,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { }) state = DownloadWizardState( + null, intent.getIntExtra("selectedLogicalSlot", 0), "", "", @@ -88,6 +96,29 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString("currentStepFragmentClassName", state.currentStepFragmentClassName) + outState.putInt("selectedLogicalSlot", state.selectedLogicalSlot) + outState.putString("smdp", state.smdp) + outState.putString("matchingId", state.matchingId) + outState.putString("confirmationCode", state.confirmationCode) + outState.putString("imei", state.imei) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + state.currentStepFragmentClassName = savedInstanceState.getString( + "currentStepFragmentClassName", + state.currentStepFragmentClassName + ) + state.selectedLogicalSlot = + savedInstanceState.getInt("selectedLogicalSlot", state.selectedLogicalSlot) + state.smdp = savedInstanceState.getString("smdp", state.smdp) + state.matchingId = savedInstanceState.getString("matchingId", state.matchingId) + state.imei = savedInstanceState.getString("imei", state.imei) + } + private fun onPrevPressed() { if (currentFragment?.hasPrev == true) { val prevFrag = currentFragment?.createPrevFragment() @@ -112,7 +143,13 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { override fun onInit() { progressBar.visibility = View.GONE - showFragment(DownloadWizardSlotSelectFragment()) + + if (state.currentStepFragmentClassName != null) { + val clazz = Class.forName(state.currentStepFragmentClassName!!) + showFragment(clazz.getDeclaredConstructor().newInstance() as DownloadWizardStepFragment) + } else { + showFragment(DownloadWizardSlotSelectFragment()) + } } private fun showFragment( From f236b40cd4ff7c72b8d2ee568d52b33b7f717f16 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Tue, 19 Nov 2024 20:49:34 -0500 Subject: [PATCH 12/33] lpac-jni: Add lookup from progress to state --- .../net/typeblog/lpac_jni/ProfileDownloadCallback.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt index 579ad58..289ddf6 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/ProfileDownloadCallback.kt @@ -1,6 +1,18 @@ package net.typeblog.lpac_jni interface ProfileDownloadCallback { + companion object { + fun lookupStateFromProgress(progress: Int): DownloadState = + when (progress) { + 0 -> DownloadState.Preparing + 20 -> DownloadState.Connecting + 40 -> DownloadState.Authenticating + 60 -> DownloadState.Downloading + 80 -> DownloadState.Finalizing + else -> throw IllegalArgumentException("Unknown state") + } + } + enum class DownloadState(val progress: Int) { Preparing(0), Connecting(20), // Before {server,client} authentication From 39b40f9b0dd0554b9705a783a979d66988021ec4 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 20 Nov 2024 20:57:35 -0500 Subject: [PATCH 13/33] ui: wizard: Lay out the download progress UI --- .../ui/wizard/DownloadWizardActivity.kt | 3 + .../wizard/DownloadWizardDetailsFragment.kt | 10 +- .../wizard/DownloadWizardProgressFragment.kt | 117 ++++++++++++++++++ .../res/drawable/ic_checkmark_outline.xml | 0 .../main/res/drawable/ic_error_outline.xml | 0 .../res/layout/download_progress_item.xml | 45 +++++++ .../res/layout/fragment_download_progress.xml | 33 +++++ app-common/src/main/res/values/strings.xml | 6 + 8 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt rename {app-unpriv => app-common}/src/main/res/drawable/ic_checkmark_outline.xml (100%) rename {app-unpriv => app-common}/src/main/res/drawable/ic_error_outline.xml (100%) create mode 100644 app-common/src/main/res/layout/download_progress_item.xml create mode 100644 app-common/src/main/res/layout/fragment_download_progress.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 029725c..d76bbe5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -132,6 +132,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { private fun onNextPressed() { if (currentFragment?.hasNext == true) { + currentFragment?.beforeNext() val nextFrag = currentFragment?.createNextFragment() if (nextFrag == null) { finish() @@ -216,5 +217,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { protected fun refreshButtons() { (requireActivity() as DownloadWizardActivity).refreshButtons() } + + open fun beforeNext() {} } } \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index c4a484f..8d9be70 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -22,10 +22,16 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF private lateinit var confirmationCode: TextInputLayout private lateinit var imei: TextInputLayout - override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? { - TODO("Not yet implemented") + override fun beforeNext() { + state.smdp = smdp.editText!!.text.toString().trim() + state.matchingId = matchingId.editText!!.text.toString().trim() + state.confirmationCode = confirmationCode.editText!!.text.toString().trim() + state.imei = imei.editText!!.text.toString() } + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment = + DownloadWizardProgressFragment() + override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment = DownloadWizardMethodSelectFragment() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt new file mode 100644 index 0000000..ed58b0d --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -0,0 +1,117 @@ +package im.angry.openeuicc.ui.wizard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import im.angry.openeuicc.common.R + +class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + private enum class ProgressState { + NotStarted, + InProgress, + Done, + Error + } + + private data class ProgressItem( + val titleRes: Int, + val state: ProgressState + ) + + private val progressItems = arrayOf( + ProgressItem(R.string.download_wizard_progress_step_preparing, ProgressState.NotStarted), + ProgressItem(R.string.download_wizard_progress_step_connecting, ProgressState.NotStarted), + ProgressItem( + R.string.download_wizard_progress_step_authenticating, + ProgressState.NotStarted + ), + ProgressItem(R.string.download_wizard_progress_step_downloading, ProgressState.NotStarted), + ProgressItem(R.string.download_wizard_progress_step_finalizing, ProgressState.NotStarted) + ) + + private val adapter = ProgressItemAdapter() + + override val hasNext: Boolean + get() = false + override val hasPrev: Boolean + get() = false + + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null + + override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_download_progress, container, false) + val recyclerView = view.requireViewById(R.id.download_progress_list) + recyclerView.adapter = adapter + recyclerView.layoutManager = + LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false) + recyclerView.addItemDecoration( + DividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL + ) + ) + return view + } + + private inner class ProgressItemHolder(val root: View) : RecyclerView.ViewHolder(root) { + private val title = root.requireViewById(R.id.download_progress_item_title) + private val progressBar = + root.requireViewById(R.id.download_progress_icon_progress) + private val icon = root.requireViewById(R.id.download_progress_icon) + + fun bind(item: ProgressItem) { + title.text = getString(item.titleRes) + + when (item.state) { + ProgressState.NotStarted -> { + progressBar.visibility = View.GONE + icon.visibility = View.GONE + } + + ProgressState.InProgress -> { + progressBar.visibility = View.VISIBLE + icon.visibility = View.GONE + } + + ProgressState.Done -> { + progressBar.visibility = View.GONE + icon.setImageResource(R.drawable.ic_checkmark_outline) + icon.visibility = View.VISIBLE + } + + ProgressState.Error -> { + progressBar.visibility = View.GONE + icon.setImageResource(R.drawable.ic_error_outline) + icon.visibility = View.VISIBLE + } + } + } + } + + private inner class ProgressItemAdapter : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressItemHolder { + val root = LayoutInflater.from(parent.context) + .inflate(R.layout.download_progress_item, parent, false) + return ProgressItemHolder(root) + } + + override fun getItemCount(): Int = progressItems.size + + override fun onBindViewHolder(holder: ProgressItemHolder, position: Int) { + holder.bind(progressItems[position]) + } + } +} \ No newline at end of file diff --git a/app-unpriv/src/main/res/drawable/ic_checkmark_outline.xml b/app-common/src/main/res/drawable/ic_checkmark_outline.xml similarity index 100% rename from app-unpriv/src/main/res/drawable/ic_checkmark_outline.xml rename to app-common/src/main/res/drawable/ic_checkmark_outline.xml diff --git a/app-unpriv/src/main/res/drawable/ic_error_outline.xml b/app-common/src/main/res/drawable/ic_error_outline.xml similarity index 100% rename from app-unpriv/src/main/res/drawable/ic_error_outline.xml rename to app-common/src/main/res/drawable/ic_error_outline.xml diff --git a/app-common/src/main/res/layout/download_progress_item.xml b/app-common/src/main/res/layout/download_progress_item.xml new file mode 100644 index 0000000..f1d0852 --- /dev/null +++ b/app-common/src/main/res/layout/download_progress_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/layout/fragment_download_progress.xml b/app-common/src/main/res/layout/fragment_download_progress.xml new file mode 100644 index 0000000..0ec58e4 --- /dev/null +++ b/app-common/src/main/res/layout/fragment_download_progress.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 3b075bb..e42bf04 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -75,6 +75,12 @@ Load a QR code from gallery Enter manually Input or confirm details for downloading your eSIM: + Downloading your eSIM… + Preparing + Establishing connection to server + Authenticating your device with server + Downloading eSIM profile + Loading eSIM profile into storage New nickname From 67c9612627389da82b1154b0691fdf360b7158fe Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 20 Nov 2024 21:00:50 -0500 Subject: [PATCH 14/33] ui: wizard: Restrict inputs to single lines --- app-common/src/main/res/layout/fragment_download_details.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app-common/src/main/res/layout/fragment_download_details.xml b/app-common/src/main/res/layout/fragment_download_details.xml index 0820c27..ca36d64 100644 --- a/app-common/src/main/res/layout/fragment_download_details.xml +++ b/app-common/src/main/res/layout/fragment_download_details.xml @@ -32,6 +32,8 @@ android:hint="@string/profile_download_server"> @@ -44,6 +46,7 @@ android:hint="@string/profile_download_code"> @@ -57,6 +60,7 @@ android:hint="@string/profile_download_confirmation_code"> @@ -73,6 +77,7 @@ app:passwordToggleEnabled="true"> From b2abe5ee8498648cb8061bc9477e1dc492382501 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Wed, 20 Nov 2024 21:03:42 -0500 Subject: [PATCH 15/33] ui: wizard: Make download details nullable --- .../openeuicc/ui/wizard/DownloadWizardActivity.kt | 12 ++++++------ .../ui/wizard/DownloadWizardDetailsFragment.kt | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index d76bbe5..6383d8b 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -19,9 +19,9 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { var currentStepFragmentClassName: String?, var selectedLogicalSlot: Int, var smdp: String, - var matchingId: String, - var confirmationCode: String, - var imei: String, + var matchingId: String?, + var confirmationCode: String?, + var imei: String?, ) private lateinit var state: DownloadWizardState @@ -53,9 +53,9 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { null, intent.getIntExtra("selectedLogicalSlot", 0), "", - "", - "", - "" + null, + null, + null ) progressBar = requireViewById(R.id.progress) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt index 8d9be70..eb36710 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDetailsFragment.kt @@ -24,9 +24,10 @@ class DownloadWizardDetailsFragment : DownloadWizardActivity.DownloadWizardStepF override fun beforeNext() { state.smdp = smdp.editText!!.text.toString().trim() - state.matchingId = matchingId.editText!!.text.toString().trim() - state.confirmationCode = confirmationCode.editText!!.text.toString().trim() - state.imei = imei.editText!!.text.toString() + // Treat empty inputs as null -- this is important for the download step + state.matchingId = matchingId.editText!!.text.toString().trim().ifBlank { null } + state.confirmationCode = confirmationCode.editText!!.text.toString().trim().ifBlank { null } + state.imei = imei.editText!!.text.toString().ifBlank { null } } override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment = From 3507c17834b72feaa5ef374ed510721088622381 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 10:18:54 -0500 Subject: [PATCH 16/33] EuiccChannalManagerService: manually buffer the returned flow --- .../service/EuiccChannelManagerService.kt | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 9cfd526..e8f3e1d 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -15,10 +15,12 @@ import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -249,29 +251,37 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // Then, we complete the returned flow, but we also set the state back to Idle. // The state update back to Idle won't show up in the returned stream, because // it has been completed by that point. - return foregroundTaskState.transformWhile { - // Also update our notification when we see an update - // But ignore the first progress = 0 update -- that is the current value. - // we need that to be handled by the main coroutine after it finishes. - if (it !is ForegroundTaskState.InProgress || it.progress != 0) { - withContext(Dispatchers.Main) { - updateForegroundNotification(title, iconRes) + val subscriberFlow = foregroundTaskState + .transformWhile { + // Also update our notification when we see an update + // But ignore the first progress = 0 update -- that is the current value. + // we need that to be handled by the main coroutine after it finishes. + if (it !is ForegroundTaskState.InProgress || it.progress != 0) { + withContext(Dispatchers.Main) { + updateForegroundNotification(title, iconRes) + } } - } - emit(it) - it !is ForegroundTaskState.Done - }.onStart { - // When this Flow is started, we unblock the coroutine launched above by - // self-starting as a foreground service. - withContext(Dispatchers.Main) { - startForegroundService( - Intent( - this@EuiccChannelManagerService, - this@EuiccChannelManagerService::class.java + emit(it) + it !is ForegroundTaskState.Done + }.onStart { + // When this Flow is started, we unblock the coroutine launched above by + // self-starting as a foreground service. + withContext(Dispatchers.Main) { + startForegroundService( + Intent( + this@EuiccChannelManagerService, + this@EuiccChannelManagerService::class.java + ) ) - ) - } - }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } + } + }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } + // Buffer the returned flow by 2, so that if there is an error, + // we always get a copy of the last process update before completion. + // This also guarantees that our onCompletion callback is always run + // even if the returned flow isn't subscribed to + .buffer(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + return subscriberFlow } val isForegroundTaskRunning: Boolean From f2c233fe1ca8a31dfe1beb47816311db0d95542e Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 10:42:02 -0500 Subject: [PATCH 17/33] EuiccChannelManagerService: Introduce IDs for tasks --- .../service/EuiccChannelManagerService.kt | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index e8f3e1d..cd6f3a8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -100,6 +100,25 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { private val foregroundTaskState: MutableStateFlow = MutableStateFlow(ForegroundTaskState.Idle) + /** + * A simple wrapper over a flow with taskId added. + * + * taskID is the exact millisecond-precision timestamp when the task is launched. + */ + class ForegroundTaskSubscriberFlow(val taskId: Long, inner: Flow) : + Flow by inner + + /** + * A cache of subscribers to 5 recently-launched foreground tasks, identified by ID + * + * Only one can be run at the same time, but those that are done will be kept in this + * map for a little while -- because UI components may be stopped and recreated while + * tasks are running. Having this buffer allows the components to re-subscribe even if + * the task completes while they are being recreated. + */ + private val foregroundTaskSubscribers: MutableMap = + mutableMapOf() + override fun onBind(intent: Intent): IBinder { super.onBind(intent) return LocalBinder() @@ -196,7 +215,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { failureTitle: String, iconRes: Int, task: suspend EuiccChannelManagerService.() -> Unit - ): Flow { + ): ForegroundTaskSubscriberFlow { + val taskID = System.currentTimeMillis() + // Atomically set the state to InProgress. If this returns true, we are // the only task currently in progress. if (!foregroundTaskState.compareAndSet( @@ -204,7 +225,9 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ForegroundTaskState.InProgress(0) ) ) { - return flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) } + return ForegroundTaskSubscriberFlow( + taskID, + flow { emit(ForegroundTaskState.Done(IllegalStateException("There are tasks currently running"))) }) } lifecycleScope.launch(Dispatchers.Main) { @@ -281,7 +304,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // even if the returned flow isn't subscribed to .buffer(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST) - return subscriberFlow + val ret = ForegroundTaskSubscriberFlow(taskID, subscriberFlow) + foregroundTaskSubscribers[taskID] = ret + + if (foregroundTaskSubscribers.size > 5) { + // Remove enough elements so that the size is kept at 5 + for (key in foregroundTaskSubscribers.keys.sorted() + .take(foregroundTaskSubscribers.size - 5)) { + foregroundTaskSubscribers.remove(key) + } + } + + return ret } val isForegroundTaskRunning: Boolean @@ -299,7 +333,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { matchingId: String?, confirmationCode: String?, imei: String? - ): Flow = + ): ForegroundTaskSubscriberFlow = launchForegroundTask( getString(R.string.task_profile_download), getString(R.string.task_profile_download_failure), @@ -335,7 +369,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { portId: Int, iccid: String, name: String - ): Flow = + ): ForegroundTaskSubscriberFlow = launchForegroundTask( getString(R.string.task_profile_rename), getString(R.string.task_profile_rename_failure), @@ -357,7 +391,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { slotId: Int, portId: Int, iccid: String - ): Flow = + ): ForegroundTaskSubscriberFlow = launchForegroundTask( getString(R.string.task_profile_delete), getString(R.string.task_profile_delete_failure), @@ -380,7 +414,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { iccid: String, enable: Boolean, // Enable or disable the profile indicated in iccid reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers - ): Flow = + ): ForegroundTaskSubscriberFlow = launchForegroundTask( getString(R.string.task_profile_switch), getString(R.string.task_profile_switch_failure), From 5b079c95ac0a8e44949494810550a7a795b6aba6 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 11:23:27 -0500 Subject: [PATCH 18/33] ui: wizard: Implement the download process --- .../service/EuiccChannelManagerService.kt | 8 ++ .../ui/wizard/DownloadWizardActivity.kt | 11 +- .../wizard/DownloadWizardProgressFragment.kt | 117 +++++++++++++++++- 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index cd6f3a8..9c40fa1 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -196,6 +196,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification) } + /** + * Recover the subscriber to a foreground task that is recently launched. + * + * null if the task doesn't exist, or was launched too long ago. + */ + fun recoverForegroundTaskSubscriber(taskId: Long): ForegroundTaskSubscriberFlow? = + foregroundTaskSubscribers[taskId] + /** * Launch a potentially blocking foreground task in this service's lifecycle context. * This function does not block, but returns a Flow that emits ForegroundTaskState diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 6383d8b..9f47e3e 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -22,6 +22,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { var matchingId: String?, var confirmationCode: String?, var imei: String?, + var downloadStarted: Boolean, + var downloadTaskID: Long, ) private lateinit var state: DownloadWizardState @@ -55,7 +57,9 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { "", null, null, - null + null, + false, + -1 ) progressBar = requireViewById(R.id.progress) @@ -104,6 +108,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { outState.putString("matchingId", state.matchingId) outState.putString("confirmationCode", state.confirmationCode) outState.putString("imei", state.imei) + outState.putBoolean("downloadStarted", state.downloadStarted) + outState.putLong("downloadTaskID", state.downloadTaskID) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -117,6 +123,9 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { state.smdp = savedInstanceState.getString("smdp", state.smdp) state.matchingId = savedInstanceState.getString("matchingId", state.matchingId) state.imei = savedInstanceState.getString("imei", state.imei) + state.downloadStarted = + savedInstanceState.getBoolean("downloadStarted", state.downloadStarted) + state.downloadTaskID = savedInstanceState.getLong("downloadTaskID", state.downloadTaskID) } private fun onPrevPressed() { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index ed58b0d..c1923f5 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -7,12 +7,32 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import im.angry.openeuicc.common.R +import im.angry.openeuicc.service.EuiccChannelManagerService +import im.angry.openeuicc.util.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import net.typeblog.lpac_jni.ProfileDownloadCallback class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + companion object { + /** + * An array of LPA-side state types, mapping 1:1 to progressItems + */ + val LPA_PROGRESS_STATES = arrayOf( + ProfileDownloadCallback.DownloadState.Preparing, + ProfileDownloadCallback.DownloadState.Connecting, + ProfileDownloadCallback.DownloadState.Authenticating, + ProfileDownloadCallback.DownloadState.Downloading, + ProfileDownloadCallback.DownloadState.Finalizing, + ) + } + private enum class ProgressState { NotStarted, InProgress, @@ -22,7 +42,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep private data class ProgressItem( val titleRes: Int, - val state: ProgressState + var state: ProgressState ) private val progressItems = arrayOf( @@ -38,8 +58,10 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep private val adapter = ProgressItemAdapter() + private var isDone = false + override val hasNext: Boolean - get() = false + get() = isDone override val hasPrev: Boolean get() = false @@ -66,6 +88,97 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep return view } + override fun onStart() { + super.onStart() + + lifecycleScope.launch { + showProgressBar(-1) // set indeterminate first + ensureEuiccChannelManager() + + val subscriber = startDownloadOrSubscribe() + + if (subscriber == null) { + requireActivity().finish() + return@launch + } + + subscriber.onEach { + when (it) { + is EuiccChannelManagerService.ForegroundTaskState.Done -> { + hideProgressBar() + + // Change the state of the last InProgress item to Error + progressItems.forEachIndexed { index, progressItem -> + if (progressItem.state == ProgressState.InProgress) { + progressItem.state = ProgressState.Error + } + + adapter.notifyItemChanged(index) + } + + isDone = true + refreshButtons() + } + + is EuiccChannelManagerService.ForegroundTaskState.InProgress -> { + updateProgress(it.progress) + } + + else -> {} + } + }.collect() + } + } + + private suspend fun startDownloadOrSubscribe(): EuiccChannelManagerService.ForegroundTaskSubscriberFlow? = + if (state.downloadStarted) { + // This will also return null if task ID is -1 (uninitialized), too + euiccChannelManagerService.recoverForegroundTaskSubscriber(state.downloadTaskID) + } else { + euiccChannelManagerService.waitForForegroundTask() + + val (slotId, portId) = euiccChannelManager.withEuiccChannel(state.selectedLogicalSlot) { channel -> + Pair(channel.slotId, channel.portId) + } + + // Set started to true even before we start -- in case we get killed in the middle + state.downloadStarted = true + + val ret = euiccChannelManagerService.launchProfileDownloadTask( + slotId, + portId, + state.smdp, + state.matchingId, + state.confirmationCode, + state.imei + ) + + state.downloadTaskID = ret.taskId + + ret + } + + private fun updateProgress(progress: Int) { + showProgressBar(progress) + + val lpaState = ProfileDownloadCallback.lookupStateFromProgress(progress) + val stateIndex = LPA_PROGRESS_STATES.indexOf(lpaState) + + if (stateIndex > 0) { + for (i in (0..(R.id.download_progress_item_title) private val progressBar = From d68a7172de6999116a97b803b4b5032b2ec23cca Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 13:14:16 -0500 Subject: [PATCH 19/33] EuiccChannelManagerService: Fix support for multiple subscribers We have to use another SharedFlow here. Otherwise, the flow transforms break our ability to subscribe to it more than once, which is needed for UI state to preserve across recreate events. --- .../service/EuiccChannelManagerService.kt | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 9c40fa1..4f6e67c 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -20,13 +20,13 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.isActive @@ -208,8 +208,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { * Launch a potentially blocking foreground task in this service's lifecycle context. * This function does not block, but returns a Flow that emits ForegroundTaskState * updates associated with this task. The last update the returned flow will emit is - * always ForegroundTaskState.Done. The returned flow MUST be started in order for the - * foreground task to run. + * always ForegroundTaskState.Done. + * + * The returned flow can be subscribed multiple times because it is implemented as a + * SharedFlow under the hood. New subscribers are guaranteed at least 1 previous update + * in addition to latest ones. This is to ensure subscribers always receive at least one + * InProgress before any sort of failure. * * The task closure is expected to update foregroundTaskState whenever appropriate. * If a foreground task is already running, this function returns null. @@ -277,42 +281,49 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } } + // This is the flow we are going to return. We allow multiple subscribers by + // re-emitting state updates into this flow from another coroutine. + // replay = 2 ensures that we at least have 1 previous state whenever subscribed to. + // This is helpful when the task completed and is then re-subscribed to due to a + // UI recreation event -- this way, the UI will know at least one last progress event + // before completion / failure + val subscriberFlow = MutableSharedFlow( + replay = 2, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + // We should be the only task running, so we can subscribe to foregroundTaskState // until we encounter ForegroundTaskState.Done. // Then, we complete the returned flow, but we also set the state back to Idle. // The state update back to Idle won't show up in the returned stream, because // it has been completed by that point. - val subscriberFlow = foregroundTaskState - .transformWhile { - // Also update our notification when we see an update - // But ignore the first progress = 0 update -- that is the current value. - // we need that to be handled by the main coroutine after it finishes. - if (it !is ForegroundTaskState.InProgress || it.progress != 0) { - withContext(Dispatchers.Main) { + lifecycleScope.launch(Dispatchers.Main) { + foregroundTaskState + .transformWhile { + // Also update our notification when we see an update + // But ignore the first progress = 0 update -- that is the current value. + // we need that to be handled by the main coroutine after it finishes. + if (it !is ForegroundTaskState.InProgress || it.progress != 0) { updateForegroundNotification(title, iconRes) } + emit(it) + it !is ForegroundTaskState.Done } - emit(it) - it !is ForegroundTaskState.Done - }.onStart { - // When this Flow is started, we unblock the coroutine launched above by - // self-starting as a foreground service. - withContext(Dispatchers.Main) { - startForegroundService( - Intent( - this@EuiccChannelManagerService, - this@EuiccChannelManagerService::class.java - ) - ) + .onEach { + subscriberFlow.emit(it) } - }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle } - // Buffer the returned flow by 2, so that if there is an error, - // we always get a copy of the last process update before completion. - // This also guarantees that our onCompletion callback is always run - // even if the returned flow isn't subscribed to - .buffer(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .onCompletion { + // Reset state back to Idle when we are done. + // We do it here because otherwise Idle and Done might become conflated + // when emitted by the main coroutine in quick succession. + // Doing it here ensures we've seen Done. This Idle event won't be + // emitted to the consumer because the subscription has completed here. + foregroundTaskState.value = ForegroundTaskState.Idle + } + .collect() + } - val ret = ForegroundTaskSubscriberFlow(taskID, subscriberFlow) + val ret = ForegroundTaskSubscriberFlow(taskID, subscriberFlow.asSharedFlow()) foregroundTaskSubscribers[taskID] = ret if (foregroundTaskSubscribers.size > 5) { @@ -323,6 +334,16 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { } } + // Before we return, and after we have set everything up, + // self-start with foreground permission. + // This is going to unblock the main coroutine handling the task. + startForegroundService( + Intent( + this@EuiccChannelManagerService, + this@EuiccChannelManagerService::class.java + ) + ) + return ret } From 74489a9ae09ccb14bd2728c062365925fb96a67c Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 13:25:11 -0500 Subject: [PATCH 20/33] EuiccChannelManagerservice: Fix completion event in returned flows --- .../service/EuiccChannelManagerService.kt | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 4f6e67c..e0d403f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first @@ -116,7 +117,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { * tasks are running. Having this buffer allows the components to re-subscribe even if * the task completes while they are being recreated. */ - private val foregroundTaskSubscribers: MutableMap = + private val foregroundTaskSubscribers: MutableMap> = mutableMapOf() override fun onBind(intent: Intent): IBinder { @@ -196,13 +197,27 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification) } + /** + * Apply transform to a ForegroundTaskState flow so that it completes when a Done is seen. + * + * This must be applied each time a flow is returned for subscription purposes. If applied + * beforehand, we lose the ability to subscribe multiple times. + */ + private fun Flow.applyCompletionTransform() = + transformWhile { + emit(it) + it !is ForegroundTaskState.Done + } + /** * Recover the subscriber to a foreground task that is recently launched. * * null if the task doesn't exist, or was launched too long ago. */ fun recoverForegroundTaskSubscriber(taskId: Long): ForegroundTaskSubscriberFlow? = - foregroundTaskSubscribers[taskId] + foregroundTaskSubscribers[taskId]?.let { + ForegroundTaskSubscriberFlow(taskId, it.applyCompletionTransform()) + } /** * Launch a potentially blocking foreground task in this service's lifecycle context. @@ -210,10 +225,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { * updates associated with this task. The last update the returned flow will emit is * always ForegroundTaskState.Done. * - * The returned flow can be subscribed multiple times because it is implemented as a - * SharedFlow under the hood. New subscribers are guaranteed at least 1 previous update - * in addition to latest ones. This is to ensure subscribers always receive at least one - * InProgress before any sort of failure. + * The returned flow can only be subscribed to once even though the underlying implementation + * is a SharedFlow. This is due to the need to apply transformations so that the stream + * actually completes. In order to subscribe multiple times, use `recoverForegroundTaskSubscriber` + * to acquire another instance. * * The task closure is expected to update foregroundTaskState whenever appropriate. * If a foreground task is already running, this function returns null. @@ -299,17 +314,15 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { // it has been completed by that point. lifecycleScope.launch(Dispatchers.Main) { foregroundTaskState - .transformWhile { + .applyCompletionTransform() + .onEach { // Also update our notification when we see an update // But ignore the first progress = 0 update -- that is the current value. // we need that to be handled by the main coroutine after it finishes. if (it !is ForegroundTaskState.InProgress || it.progress != 0) { updateForegroundNotification(title, iconRes) } - emit(it) - it !is ForegroundTaskState.Done - } - .onEach { + subscriberFlow.emit(it) } .onCompletion { @@ -323,8 +336,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { .collect() } - val ret = ForegroundTaskSubscriberFlow(taskID, subscriberFlow.asSharedFlow()) - foregroundTaskSubscribers[taskID] = ret + foregroundTaskSubscribers[taskID] = subscriberFlow.asSharedFlow() if (foregroundTaskSubscribers.size > 5) { // Remove enough elements so that the size is kept at 5 @@ -344,7 +356,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ) ) - return ret + return ForegroundTaskSubscriberFlow( + taskID, + subscriberFlow.asSharedFlow().applyCompletionTransform() + ) } val isForegroundTaskRunning: Boolean From 1a3fd621d920c8e1086d97d8cc10051c9e2a293b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 15:34:36 -0500 Subject: [PATCH 21/33] EuiccChannelManagerService: move applyCompletionTransform() to companion object --- .../service/EuiccChannelManagerService.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index e0d403f..87b5e48 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -68,6 +68,18 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { */ suspend fun Flow.waitDone(): Throwable? = (this.last() as ForegroundTaskState.Done).error + + /** + * Apply transform to a ForegroundTaskState flow so that it completes when a Done is seen. + * + * This must be applied each time a flow is returned for subscription purposes. If applied + * beforehand, we lose the ability to subscribe multiple times. + */ + private fun Flow.applyCompletionTransform() = + transformWhile { + emit(it) + it !is ForegroundTaskState.Done + } } inner class LocalBinder : Binder() { @@ -197,18 +209,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification) } - /** - * Apply transform to a ForegroundTaskState flow so that it completes when a Done is seen. - * - * This must be applied each time a flow is returned for subscription purposes. If applied - * beforehand, we lose the ability to subscribe multiple times. - */ - private fun Flow.applyCompletionTransform() = - transformWhile { - emit(it) - it !is ForegroundTaskState.Done - } - /** * Recover the subscriber to a foreground task that is recently launched. * From 895cbdd53d564075e788be24b47147bd6f9d6ef7 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 15:56:44 -0500 Subject: [PATCH 22/33] lpa: Track last HTTP response on failure We'll have a "error diagnosis" page for the new download wizard. We'll probably want to do this for APDU too. --- .../service/EuiccChannelManagerService.kt | 18 ++++++++++++------ .../net/typeblog/lpac_jni/HttpInterface.kt | 9 +++++++++ .../typeblog/lpac_jni/LocalProfileAssistant.kt | 3 +++ .../lpac_jni/impl/HttpInterfaceImpl.kt | 6 +++++- .../lpac_jni/impl/LocalProfileAssistantImpl.kt | 5 ++++- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 87b5e48..6a995c8 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.yield +import net.typeblog.lpac_jni.HttpInterface import net.typeblog.lpac_jni.ProfileDownloadCallback /** @@ -370,6 +371,10 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { .collect() } + data class ProfileDownloadException( + val lastHttpResponse: HttpInterface.HttpResponse? + ) : Exception("Failed to download profile") + fun launchProfileDownloadTask( slotId: Int, portId: Int, @@ -384,8 +389,8 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { R.drawable.ic_task_sim_card_download ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { - val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - channel.lpa.downloadProfile( + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + val res = channel.lpa.downloadProfile( smdp, matchingId, imei, @@ -397,11 +402,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ForegroundTaskState.InProgress(state.progress) } }) - } - if (!res) { - // TODO: Provide more details on the error - throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.") + if (!res) { + throw ProfileDownloadException( + channel.lpa.lastHttpResponse + ) + } } preferenceRepository.notificationDownloadFlow.first() diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt index 49d6cc0..53b9c4f 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt @@ -26,6 +26,15 @@ interface HttpInterface { } } + /** + * The last HTTP response we have received from the SM-DP+ server. + * + * This is intended for error diagnosis. However, note that most SM-DP+ servers + * respond with 200 even when there is an error. This needs to be taken into + * account when designing UI. + */ + val lastHttpResponse: HttpResponse? + fun transmit(url: String, tx: ByteArray, headers: Array): HttpResponse // The LPA is supposed to pass in a list of pkIds supported by the eUICC. // HttpInterface is responsible for providing TrustManager implementations that diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index f256caf..ed7ae6e 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -1,5 +1,7 @@ package net.typeblog.lpac_jni +import net.typeblog.lpac_jni.HttpInterface.HttpResponse + interface LocalProfileAssistant { val valid: Boolean val profiles: List @@ -7,6 +9,7 @@ interface LocalProfileAssistant { val eID: String // Extended EuiccInfo for use with LUIs, containing information such as firmware version val euiccInfo2: EuiccInfo2? + val lastHttpResponse: HttpResponse? /** * Set the max segment size (mss) for all es10x commands. This can help with removable diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt index 77227f8..68781bf 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt @@ -23,6 +23,8 @@ class HttpInterfaceImpl( private lateinit var trustManagers: Array + override var lastHttpResponse: HttpInterface.HttpResponse? = null + override fun transmit( url: String, tx: ByteArray, @@ -73,7 +75,9 @@ class HttpInterfaceImpl( } } - return HttpInterface.HttpResponse(conn.responseCode, bytes) + return HttpInterface.HttpResponse(conn.responseCode, bytes).also { + lastHttpResponse = it + } } catch (e: Exception) { e.printStackTrace() throw e diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 51039cd..6b8497d 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -12,7 +12,7 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( private val apduInterface: ApduInterface, - httpInterface: HttpInterface + private val httpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" @@ -34,6 +34,9 @@ class LocalProfileAssistantImpl( LpacJni.euiccSetMss(contextHandle, mss) } + override val lastHttpResponse: HttpInterface.HttpResponse? + get() = httpInterface.lastHttpResponse + override val valid: Boolean get() = !finalized && apduInterface.valid && try { // If we can read both eID and euiccInfo2 properly, we are likely looking at From 07072667db604bf771fc9f72ca2c2fabad5805d6 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 17:10:46 -0500 Subject: [PATCH 23/33] ui: wizard: Add error diagnostics We finally have it!!! --- .../core/LocalProfileAssistantWrapper.kt | 4 ++ .../ui/wizard/DownloadWizardActivity.kt | 5 +- .../DownloadWizardDiagnosticsFragment.kt | 67 +++++++++++++++++++ .../wizard/DownloadWizardProgressFragment.kt | 10 ++- .../layout/fragment_download_diagnostics.xml | 48 +++++++++++++ app-common/src/main/res/values/strings.xml | 3 + 6 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt create mode 100644 app-common/src/main/res/layout/fragment_download_diagnostics.xml diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index e6a648a..d07438f 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -1,6 +1,7 @@ package im.angry.openeuicc.core import net.typeblog.lpac_jni.EuiccInfo2 +import net.typeblog.lpac_jni.HttpInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification @@ -19,6 +20,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : return _inner!! } + override val lastHttpResponse: HttpInterface.HttpResponse? + get() = lpa.lastHttpResponse + override val valid: Boolean get() = lpa.valid override val profiles: List diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 9f47e3e..6a1c116 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -11,6 +11,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import im.angry.openeuicc.common.R +import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.ui.BaseEuiccAccessActivity import im.angry.openeuicc.util.* @@ -24,6 +25,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { var imei: String?, var downloadStarted: Boolean, var downloadTaskID: Long, + var downloadError: EuiccChannelManagerService.ProfileDownloadException?, ) private lateinit var state: DownloadWizardState @@ -59,7 +61,8 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { null, null, false, - -1 + -1, + null ) progressBar = requireViewById(R.id.progress) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt new file mode 100644 index 0000000..f077dc9 --- /dev/null +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -0,0 +1,67 @@ +package im.angry.openeuicc.ui.wizard + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import im.angry.openeuicc.common.R + +class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { + override val hasNext: Boolean + get() = true + override val hasPrev: Boolean + get() = false + + private lateinit var diagnosticTextView: TextView + + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null + + override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_download_diagnostics, container, false) + diagnosticTextView = view.requireViewById(R.id.download_wizard_diagnostics_text) + return view + } + + override fun onStart() { + super.onStart() + val str = buildDiagnosticsText() + if (str == null) { + requireActivity().finish() + return + } + + diagnosticTextView.text = str + } + + private fun buildDiagnosticsText(): String? = state.downloadError?.let { err -> + val ret = StringBuilder() + + err.lastHttpResponse?.let { resp -> + if (resp.rcode != 200) { + // Only show the status if it's not 200 + // Because we can have errors even if the rcode is 200 due to SM-DP+ servers being dumb + // and showing 200 might mislead users + ret.appendLine( + getString( + R.string.download_wizard_diagnostics_last_http_status, + resp.rcode + ) + ) + ret.appendLine() + } + + ret.appendLine(getString(R.string.download_wizard_diagnostics_last_http_response)) + ret.appendLine() + ret.appendLine(resp.data.decodeToString(throwOnInvalidSequence = false)) + } + + ret.toString() + } +} \ No newline at end of file diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index c1923f5..e4d4437 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -65,7 +65,12 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep override val hasPrev: Boolean get() = false - override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null + override fun createNextFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = + if (state.downloadError != null) { + DownloadWizardDiagnosticsFragment() + } else { + null + } override fun createPrevFragment(): DownloadWizardActivity.DownloadWizardStepFragment? = null @@ -116,6 +121,9 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep adapter.notifyItemChanged(index) } + state.downloadError = + it.error as? EuiccChannelManagerService.ProfileDownloadException + isDone = true refreshButtons() } diff --git a/app-common/src/main/res/layout/fragment_download_diagnostics.xml b/app-common/src/main/res/layout/fragment_download_diagnostics.xml new file mode 100644 index 0000000..b9a0bc2 --- /dev/null +++ b/app-common/src/main/res/layout/fragment_download_diagnostics.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index e42bf04..e8ffb80 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -81,6 +81,9 @@ Authenticating your device with server Downloading eSIM profile Loading eSIM profile into storage + Error diagnostics + Last HTTP status: %d + Last HTTP response: New nickname From 74d7da35dce724a6cdfba9c994cb748e1ed80218 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 17:29:51 -0500 Subject: [PATCH 24/33] ui: wizard: Quick and dirty JSON pretty-printer --- .../DownloadWizardDiagnosticsFragment.kt | 11 ++- .../im/angry/openeuicc/util/StringUtils.kt | 67 ++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index f077dc9..b334ab9 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import im.angry.openeuicc.common.R +import im.angry.openeuicc.util.* class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardStepFragment() { override val hasNext: Boolean @@ -59,7 +60,15 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS ret.appendLine(getString(R.string.download_wizard_diagnostics_last_http_response)) ret.appendLine() - ret.appendLine(resp.data.decodeToString(throwOnInvalidSequence = false)) + + val str = resp.data.decodeToString(throwOnInvalidSequence = false) + ret.appendLine( + if (str.startsWith('{')) { + str.prettyPrintJson() + } else { + str + } + ) } ret.toString() diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index ebf8729..44c7825 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -27,4 +27,69 @@ fun formatFreeSpace(size: Int): String = "%.2f KiB".format(size.toDouble() / 1024) } else { "$size B" - } \ No newline at end of file + } + +fun String.prettyPrintJson(): String { + val ret = StringBuilder() + var inQuotes = false + var escaped = false + val indentSymbolStack = ArrayDeque() + + val addNewLine = { + ret.append('\n') + repeat(indentSymbolStack.size) { + ret.append('\t') + } + } + + var lastChar = ' ' + + for (c in this) { + when { + !inQuotes && (c == '{' || c == '[') -> { + ret.append(c) + indentSymbolStack.addLast(c) + addNewLine() + } + + !inQuotes && (c == '}' || c == ']') -> { + indentSymbolStack.removeLast() + if (lastChar != ',') { + addNewLine() + } + ret.append(c) + } + + !inQuotes && c == ',' -> { + ret.append(c) + addNewLine() + } + + !inQuotes && c == ':' -> { + ret.append(c) + ret.append(' ') + } + + inQuotes && c == '\\' -> { + ret.append(c) + escaped = true + continue + } + + !escaped && c == '"' -> { + ret.append(c) + inQuotes = !inQuotes + } + + else -> ret.append(c) + } + + if (escaped) { + escaped = false + } + + lastChar = c + } + + return ret.toString() +} \ No newline at end of file From 426e5c0197ef156f19d878a8f14fc665c09eb85a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 17:45:11 -0500 Subject: [PATCH 25/33] util: Ignore spaces in JSON string --- .../src/main/java/im/angry/openeuicc/util/StringUtils.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt index 44c7825..8d72462 100644 --- a/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt +++ b/app-common/src/main/java/im/angry/openeuicc/util/StringUtils.kt @@ -81,6 +81,11 @@ fun String.prettyPrintJson(): String { inQuotes = !inQuotes } + !inQuotes && c == ' ' -> { + // Do nothing -- we ignore spaces outside of quotes by default + // This is to ensure predictable formatting + } + else -> ret.append(c) } From 5476e335b19fc19e4a40982a7429b1759ff7755d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:20:47 -0500 Subject: [PATCH 26/33] Move ProfileDownloadException to LPA --- .../openeuicc/core/LocalProfileAssistantWrapper.kt | 6 +----- .../openeuicc/service/EuiccChannelManagerService.kt | 13 +------------ .../openeuicc/ui/wizard/DownloadWizardActivity.kt | 4 ++-- .../ui/wizard/DownloadWizardProgressFragment.kt | 3 ++- .../net/typeblog/lpac_jni/LocalProfileAssistant.kt | 7 +++++-- .../lpac_jni/impl/LocalProfileAssistantImpl.kt | 13 +++++++------ 6 files changed, 18 insertions(+), 28 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index d07438f..22ece46 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -1,7 +1,6 @@ package im.angry.openeuicc.core import net.typeblog.lpac_jni.EuiccInfo2 -import net.typeblog.lpac_jni.HttpInterface import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification @@ -20,9 +19,6 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : return _inner!! } - override val lastHttpResponse: HttpInterface.HttpResponse? - get() = lpa.lastHttpResponse - override val valid: Boolean get() = lpa.valid override val profiles: List @@ -50,7 +46,7 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : imei: String?, confirmationCode: String?, callback: ProfileDownloadCallback - ): Boolean = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) + ) = lpa.downloadProfile(smdp, matchingId, imei, confirmationCode, callback) override fun deleteNotification(seqNumber: Long): Boolean = lpa.deleteNotification(seqNumber) diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index 6a995c8..c4d16df 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -35,7 +35,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.yield -import net.typeblog.lpac_jni.HttpInterface import net.typeblog.lpac_jni.ProfileDownloadCallback /** @@ -371,10 +370,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { .collect() } - data class ProfileDownloadException( - val lastHttpResponse: HttpInterface.HttpResponse? - ) : Exception("Failed to download profile") - fun launchProfileDownloadTask( slotId: Int, portId: Int, @@ -390,7 +385,7 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ) { euiccChannelManager.beginTrackedOperation(slotId, portId) { euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> - val res = channel.lpa.downloadProfile( + channel.lpa.downloadProfile( smdp, matchingId, imei, @@ -402,12 +397,6 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { ForegroundTaskState.InProgress(state.progress) } }) - - if (!res) { - throw ProfileDownloadException( - channel.lpa.lastHttpResponse - ) - } } preferenceRepository.notificationDownloadFlow.first() diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt index 6a1c116..6d810cf 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardActivity.kt @@ -11,9 +11,9 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import im.angry.openeuicc.common.R -import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.ui.BaseEuiccAccessActivity import im.angry.openeuicc.util.* +import net.typeblog.lpac_jni.LocalProfileAssistant class DownloadWizardActivity: BaseEuiccAccessActivity() { data class DownloadWizardState( @@ -25,7 +25,7 @@ class DownloadWizardActivity: BaseEuiccAccessActivity() { var imei: String?, var downloadStarted: Boolean, var downloadTaskID: Long, - var downloadError: EuiccChannelManagerService.ProfileDownloadException?, + var downloadError: LocalProfileAssistant.ProfileDownloadException?, ) private lateinit var state: DownloadWizardState diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt index e4d4437..f6f63fd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardProgressFragment.kt @@ -17,6 +17,7 @@ import im.angry.openeuicc.util.* import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.ProfileDownloadCallback class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStepFragment() { @@ -122,7 +123,7 @@ class DownloadWizardProgressFragment : DownloadWizardActivity.DownloadWizardStep } state.downloadError = - it.error as? EuiccChannelManagerService.ProfileDownloadException + it.error as? LocalProfileAssistant.ProfileDownloadException isDone = true refreshButtons() diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index ed7ae6e..e5d7b20 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -3,13 +3,16 @@ package net.typeblog.lpac_jni import net.typeblog.lpac_jni.HttpInterface.HttpResponse interface LocalProfileAssistant { + data class ProfileDownloadException( + val lastHttpResponse: HttpResponse? + ) : Exception("Failed to download profile") + val valid: Boolean val profiles: List val notifications: List val eID: String // Extended EuiccInfo for use with LUIs, containing information such as firmware version val euiccInfo2: EuiccInfo2? - val lastHttpResponse: HttpResponse? /** * Set the max segment size (mss) for all es10x commands. This can help with removable @@ -25,7 +28,7 @@ interface LocalProfileAssistant { fun deleteProfile(iccid: String): Boolean fun downloadProfile(smdp: String, matchingId: String?, imei: String?, - confirmationCode: String?, callback: ProfileDownloadCallback): Boolean + confirmationCode: String?, callback: ProfileDownloadCallback) fun deleteNotification(seqNumber: Long): Boolean fun handleNotification(seqNumber: Long): Boolean diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 6b8497d..3cacc3d 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -34,9 +34,6 @@ class LocalProfileAssistantImpl( LpacJni.euiccSetMss(contextHandle, mss) } - override val lastHttpResponse: HttpInterface.HttpResponse? - get() = httpInterface.lastHttpResponse - override val valid: Boolean get() = !finalized && apduInterface.valid && try { // If we can read both eID and euiccInfo2 properly, we are likely looking at @@ -147,15 +144,19 @@ class LocalProfileAssistantImpl( @Synchronized override fun downloadProfile(smdp: String, matchingId: String?, imei: String?, - confirmationCode: String?, callback: ProfileDownloadCallback): Boolean { - return LpacJni.downloadProfile( + confirmationCode: String?, callback: ProfileDownloadCallback) { + val res = LpacJni.downloadProfile( contextHandle, smdp, matchingId, imei, confirmationCode, callback - ) == 0 + ) + + if (res != 0) { + throw LocalProfileAssistant.ProfileDownloadException(httpInterface.lastHttpResponse) + } } @Synchronized From 26d037048d96e3a0bb9dd4c7eadb40f427c0d605 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:26:47 -0500 Subject: [PATCH 27/33] ui: wizard: Show HTTP exception in diagnostics --- .../ui/wizard/DownloadWizardDiagnosticsFragment.kt | 10 ++++++++++ app-common/src/main/res/values/strings.xml | 1 + .../main/java/net/typeblog/lpac_jni/HttpInterface.kt | 5 +++++ .../net/typeblog/lpac_jni/LocalProfileAssistant.kt | 3 ++- .../net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt | 6 ++++++ .../lpac_jni/impl/LocalProfileAssistantImpl.kt | 5 ++++- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index b334ab9..a870619 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -69,6 +69,16 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS str } ) + + ret.appendLine() + } + + err.lastHttpException?.let { e -> + ret.appendLine(getString(R.string.download_wizard_diagnostics_last_http_exception)) + ret.appendLine() + ret.appendLine("${e.javaClass.name}: ${e.message}") + ret.appendLine(e.stackTrace.joinToString("\n")) + ret.appendLine() } ret.toString() diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index e8ffb80..90062da 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -84,6 +84,7 @@ Error diagnostics Last HTTP status: %d Last HTTP response: + Last HTTP exception: New nickname diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt index 53b9c4f..7a41102 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt @@ -35,6 +35,11 @@ interface HttpInterface { */ val lastHttpResponse: HttpResponse? + /** + * The last exception that has been thrown during a HTTP connection + */ + val lastHttpException: Exception? + fun transmit(url: String, tx: ByteArray, headers: Array): HttpResponse // The LPA is supposed to pass in a list of pkIds supported by the eUICC. // HttpInterface is responsible for providing TrustManager implementations that diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index e5d7b20..c78f5f2 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -4,7 +4,8 @@ import net.typeblog.lpac_jni.HttpInterface.HttpResponse interface LocalProfileAssistant { data class ProfileDownloadException( - val lastHttpResponse: HttpResponse? + val lastHttpResponse: HttpResponse?, + val lastHttpException: Exception?, ) : Exception("Failed to download profile") val valid: Boolean diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt index 68781bf..7c251b7 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt @@ -24,6 +24,7 @@ class HttpInterfaceImpl( private lateinit var trustManagers: Array override var lastHttpResponse: HttpInterface.HttpResponse? = null + override var lastHttpException: Exception? = null override fun transmit( url: String, @@ -80,6 +81,11 @@ class HttpInterfaceImpl( } } catch (e: Exception) { e.printStackTrace() + + // Reset response to null because there's no response here + lastHttpResponse = null + lastHttpException = e + throw e } } diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index 3cacc3d..b568ce1 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -155,7 +155,10 @@ class LocalProfileAssistantImpl( ) if (res != 0) { - throw LocalProfileAssistant.ProfileDownloadException(httpInterface.lastHttpResponse) + throw LocalProfileAssistant.ProfileDownloadException( + httpInterface.lastHttpResponse, + httpInterface.lastHttpException + ) } } From 326b39ed05ac221825e7191638643fe6667b054d Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:43:42 -0500 Subject: [PATCH 28/33] ui: wizard: Add APDU errors to diagnostics --- .../DownloadWizardDiagnosticsFragment.kt | 27 ++++++++++++++++++ app-common/src/main/res/values/strings.xml | 4 +++ .../lpac_jni/LocalProfileAssistant.kt | 3 ++ .../impl/LocalProfileAssistantImpl.kt | 28 +++++++++++++++++-- 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index a870619..a820f05 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -81,6 +81,33 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS ret.appendLine() } + err.lastApduResponse?.let { resp -> + ret.appendLine( + getString( + R.string.download_wizard_diagnostics_last_apdu_response, + resp.encodeHex() + ) + ) + ret.appendLine() + + val isSuccess = + resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte() + + if (isSuccess) { + ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_response_success)) + } else { + ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_response_fail)) + } + } + + err.lastApduException?.let { e -> + ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_exception)) + ret.appendLine() + ret.appendLine("${e.javaClass.name}: ${e.message}") + ret.appendLine(e.stackTrace.joinToString("\n")) + ret.appendLine() + } + ret.toString() } } \ No newline at end of file diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 90062da..8d003c0 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -85,6 +85,10 @@ Last HTTP status: %d Last HTTP response: Last HTTP exception: + Last APDU response: %s + Last APDU response is successful + Last APDU response is a failure + Last APDU exception: New nickname diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index c78f5f2..cf870e1 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -3,9 +3,12 @@ package net.typeblog.lpac_jni import net.typeblog.lpac_jni.HttpInterface.HttpResponse interface LocalProfileAssistant { + @Suppress("ArrayInDataClass") data class ProfileDownloadException( val lastHttpResponse: HttpResponse?, val lastHttpException: Exception?, + val lastApduResponse: ByteArray?, + val lastApduException: Exception?, ) : Exception("Failed to download profile") val valid: Boolean diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index b568ce1..d4061a3 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -11,13 +11,35 @@ import net.typeblog.lpac_jni.LocalProfileNotification import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( - private val apduInterface: ApduInterface, + rawApduInterface: ApduInterface, private val httpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" } + /** + * A thin wrapper over ApduInterface to acquire exceptions and errors transparently + */ + private class ApduInterfaceWrapper(val apduInterface: ApduInterface) : + ApduInterface by apduInterface { + var lastApduResponse: ByteArray? = null + var lastApduException: Exception? = null + + override fun transmit(tx: ByteArray): ByteArray = + try { + apduInterface.transmit(tx).also { + lastApduResponse = it + } + } catch (e: Exception) { + lastApduResponse = null + lastApduException = e + throw e + } + } + + private val apduInterface = ApduInterfaceWrapper(rawApduInterface) + private var finalized = false private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) @@ -157,7 +179,9 @@ class LocalProfileAssistantImpl( if (res != 0) { throw LocalProfileAssistant.ProfileDownloadException( httpInterface.lastHttpResponse, - httpInterface.lastHttpException + httpInterface.lastHttpException, + apduInterface.lastApduResponse, + apduInterface.lastApduException, ) } } From d7214141e681350c4d61809c3945343769775f53 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:45:05 -0500 Subject: [PATCH 29/33] ui: wizard: Only show full APDU response when it is a failure --- .../DownloadWizardDiagnosticsFragment.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt index a820f05..6c578dd 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardDiagnosticsFragment.kt @@ -82,20 +82,22 @@ class DownloadWizardDiagnosticsFragment : DownloadWizardActivity.DownloadWizardS } err.lastApduResponse?.let { resp -> - ret.appendLine( - getString( - R.string.download_wizard_diagnostics_last_apdu_response, - resp.encodeHex() - ) - ) - ret.appendLine() - val isSuccess = resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte() if (isSuccess) { ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_response_success)) } else { + // Only show the full APDU response when it's a failure + // Otherwise it's going to get very crammed + ret.appendLine( + getString( + R.string.download_wizard_diagnostics_last_apdu_response, + resp.encodeHex() + ) + ) + ret.appendLine() + ret.appendLine(getString(R.string.download_wizard_diagnostics_last_apdu_response_fail)) } } From 1c4263a47a07d00e2eb8e916c2577f3f0405ad80 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:45:45 -0500 Subject: [PATCH 30/33] ui: wizard: Make clear what HTTP and APDU mean --- app-common/src/main/res/values/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 8d003c0..a3067dd 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -82,12 +82,12 @@ Downloading eSIM profile Loading eSIM profile into storage Error diagnostics - Last HTTP status: %d - Last HTTP response: + Last HTTP status (from server): %d + Last HTTP response (from server): Last HTTP exception: - Last APDU response: %s - Last APDU response is successful - Last APDU response is a failure + Last APDU response (from SIM): %s + Last APDU response (from SIM) is successful + Last APDU response (from SIM) is a failure Last APDU exception: New nickname From dcae65011eaf8f746c6fe43cfa4062fb0d1849f8 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:49:47 -0500 Subject: [PATCH 31/33] lpac_jni: Move HTTP diagnostics to LPA --- .../net/typeblog/lpac_jni/HttpInterface.kt | 14 -------- .../lpac_jni/impl/HttpInterfaceImpl.kt | 12 +------ .../impl/LocalProfileAssistantImpl.kt | 35 ++++++++++++++++++- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt index 7a41102..49d6cc0 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/HttpInterface.kt @@ -26,20 +26,6 @@ interface HttpInterface { } } - /** - * The last HTTP response we have received from the SM-DP+ server. - * - * This is intended for error diagnosis. However, note that most SM-DP+ servers - * respond with 200 even when there is an error. This needs to be taken into - * account when designing UI. - */ - val lastHttpResponse: HttpResponse? - - /** - * The last exception that has been thrown during a HTTP connection - */ - val lastHttpException: Exception? - fun transmit(url: String, tx: ByteArray, headers: Array): HttpResponse // The LPA is supposed to pass in a list of pkIds supported by the eUICC. // HttpInterface is responsible for providing TrustManager implementations that diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt index 7c251b7..77227f8 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/HttpInterfaceImpl.kt @@ -23,9 +23,6 @@ class HttpInterfaceImpl( private lateinit var trustManagers: Array - override var lastHttpResponse: HttpInterface.HttpResponse? = null - override var lastHttpException: Exception? = null - override fun transmit( url: String, tx: ByteArray, @@ -76,16 +73,9 @@ class HttpInterfaceImpl( } } - return HttpInterface.HttpResponse(conn.responseCode, bytes).also { - lastHttpResponse = it - } + return HttpInterface.HttpResponse(conn.responseCode, bytes) } catch (e: Exception) { e.printStackTrace() - - // Reset response to null because there's no response here - lastHttpResponse = null - lastHttpException = e - throw e } } diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index d4061a3..ece19b3 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -5,6 +5,7 @@ import net.typeblog.lpac_jni.LpacJni import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.EuiccInfo2 import net.typeblog.lpac_jni.HttpInterface +import net.typeblog.lpac_jni.HttpInterface.HttpResponse import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileNotification @@ -12,7 +13,7 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback class LocalProfileAssistantImpl( rawApduInterface: ApduInterface, - private val httpInterface: HttpInterface + rawHttpInterface: HttpInterface ): LocalProfileAssistant { companion object { private const val TAG = "LocalProfileAssistantImpl" @@ -38,7 +39,39 @@ class LocalProfileAssistantImpl( } } + /** + * Same for HTTP for diagnostics + */ + private class HttpInterfaceWrapper(val httpInterface: HttpInterface) : + HttpInterface by httpInterface { + /** + * The last HTTP response we have received from the SM-DP+ server. + * + * This is intended for error diagnosis. However, note that most SM-DP+ servers + * respond with 200 even when there is an error. This needs to be taken into + * account when designing UI. + */ + var lastHttpResponse: HttpResponse? = null + + /** + * The last exception that has been thrown during a HTTP connection + */ + var lastHttpException: Exception? = null + + override fun transmit(url: String, tx: ByteArray, headers: Array): HttpResponse = + try { + httpInterface.transmit(url, tx, headers).also { + lastHttpResponse = it + } + } catch (e: Exception) { + lastHttpResponse = null + lastHttpException = e + throw e + } + } + private val apduInterface = ApduInterfaceWrapper(rawApduInterface) + private val httpInterface = HttpInterfaceWrapper(rawHttpInterface) private var finalized = false private var contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) From 96bc9865ff8362837f632ac37cc44ca41d66bf80 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 24 Nov 2024 19:50:27 -0500 Subject: [PATCH 32/33] lpac_jni: Clear exceptions before setting response --- .../net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index ece19b3..70606d9 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -30,6 +30,7 @@ class LocalProfileAssistantImpl( override fun transmit(tx: ByteArray): ByteArray = try { apduInterface.transmit(tx).also { + lastApduException = null lastApduResponse = it } } catch (e: Exception) { @@ -61,6 +62,7 @@ class LocalProfileAssistantImpl( override fun transmit(url: String, tx: ByteArray, headers: Array): HttpResponse = try { httpInterface.transmit(url, tx, headers).also { + lastHttpException = null lastHttpResponse = it } } catch (e: Exception) { From 90878438f90659a46b5aa0e0f72ff21d9ad40d3c Mon Sep 17 00:00:00 2001 From: septs Date: Wed, 27 Nov 2024 15:06:32 +0100 Subject: [PATCH 33/33] refactor: gitignore (#69) Rebuild root dot-gitignore file Update subdirectory dot-gitignore file Reviewed-on: https://gitea.angry.im/PeterCxy/OpenEUICC/pulls/69 Co-authored-by: septs Co-committed-by: septs --- .gitignore | 22 ++++++++++------------ .idea/.gitignore | 13 ++++++++----- .idea/gradle.xml | 39 --------------------------------------- .idea/misc.xml | 25 ------------------------- app-deps/.gitignore | 3 ++- buildSrc/.gitignore | 2 ++ libs/lpac-jni/.gitignore | 1 + 7 files changed, 23 insertions(+), 82 deletions(-) delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/misc.xml create mode 100644 buildSrc/.gitignore create mode 100644 libs/lpac-jni/.gitignore diff --git a/.gitignore b/.gitignore index 6cc80a0..1aa6f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,11 @@ -*.iml -.gradle -/local.properties -/keystore.properties -.DS_Store -/build +/.gradle /captures -.externalNativeBuild -.cxx -local.properties -/libs/**/build -/buildSrc/build -/app-deps/libs \ No newline at end of file + +# Configuration files + +/keystore.properties +/local.properties + +# macOS + +.DS_Store diff --git a/.idea/.gitignore b/.idea/.gitignore index c9924ab..0d51aca 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,10 +1,13 @@ -# Default ignored files /shelf /caches /libraries -/modules.xml -/workspace.xml -/navEditor.xml /assetWizardSettings.xml /deploymentTargetDropDown.xml -/runConfigurations.xml \ No newline at end of file +/gradle.xml +/misc.xml +/modules.xml +/navEditor.xml +/runConfigurations.xml +/workspace.xml + +**/*.iml \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 589fc6c..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index a1f9d9f..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app-deps/.gitignore b/app-deps/.gitignore index 42afabf..c23e5a2 100644 --- a/app-deps/.gitignore +++ b/app-deps/.gitignore @@ -1 +1,2 @@ -/build \ No newline at end of file +/build +/libs \ No newline at end of file diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 0000000..6fbe8a4 --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1,2 @@ +/.gradle +/build \ No newline at end of file diff --git a/libs/lpac-jni/.gitignore b/libs/lpac-jni/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/libs/lpac-jni/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file