浏览代码

implement eUICC profile downloading

hope that this actually works...
Peter Cai 3 年之前
父节点
当前提交
df46ed883b

+ 2 - 1
app/src/main/AndroidManifest.xml

@@ -17,7 +17,8 @@
         android:label="@string/app_name"
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
-        android:theme="@style/Theme.OpenEUICC">
+        android:theme="@style/Theme.OpenEUICC"
+        android:networkSecurityConfig="@xml/network_security_config">
         <activity
             android:name=".ui.MainActivity"
             android:exported="true">

+ 5 - 1
app/src/main/java/im/angry/openeuicc/ui/EuiccChannelFragmentUtils.kt

@@ -20,4 +20,8 @@ val <T> T.slotId: Int where T: Fragment, T: EuiccFragmentMarker
 
 val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccFragmentMarker
     get() =
-        (requireActivity().application as OpenEUICCApplication).euiccChannelRepo.availableChannels[slotId]
+        (requireActivity().application as OpenEUICCApplication).euiccChannelRepo.availableChannels[slotId]
+
+interface EuiccProfilesChangedListener {
+    fun onEuiccProfilesChanged()
+}

+ 5 - 1
app/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -17,7 +17,7 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
-class EuiccManagementFragment : Fragment(), EuiccFragmentMarker {
+class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
     companion object {
         fun newInstance(slotId: Int): EuiccManagementFragment =
             newInstanceEuicc(EuiccManagementFragment::class.java, slotId)
@@ -55,6 +55,10 @@ class EuiccManagementFragment : Fragment(), EuiccFragmentMarker {
         refresh()
     }
 
+    override fun onEuiccProfilesChanged() {
+        refresh()
+    }
+
     @SuppressLint("NotifyDataSetChanged")
     private fun refresh() {
         binding.swipeRefresh.isRefreshing = true

+ 68 - 2
app/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt

@@ -2,14 +2,22 @@ package im.angry.openeuicc.ui
 
 import android.app.Dialog
 import android.os.Bundle
+import android.util.Log
 import android.view.*
+import android.widget.Toast
 import androidx.appcompat.widget.Toolbar
 import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.lifecycleScope
 import com.journeyapps.barcodescanner.ScanContract
 import com.journeyapps.barcodescanner.ScanOptions
+import com.truphone.lpa.progress.DownloadProgress
 import im.angry.openeuicc.R
 import im.angry.openeuicc.databinding.FragmentProfileDownloadBinding
 import im.angry.openeuicc.util.setWidthPercent
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.lang.Exception
 
 class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.OnMenuItemClickListener {
     companion object {
@@ -22,6 +30,8 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
     private var _binding: FragmentProfileDownloadBinding? = null
     private val binding get() = _binding!!
 
+    private var downloading = false
+
     private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
         result.contents?.let { content ->
             val components = content.split("$")
@@ -46,13 +56,13 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
         binding.toolbar.apply {
             setTitle(R.string.profile_download)
             setNavigationOnClickListener {
-                dismiss()
+                if (!downloading) dismiss()
             }
             setOnMenuItemClickListener(this@ProfileDownloadFragment)
         }
     }
 
-    override fun onMenuItemClick(item: MenuItem): Boolean =
+    override fun onMenuItemClick(item: MenuItem): Boolean = downloading ||
         when (item.itemId) {
             R.id.scan -> {
                 barcodeScannerLauncher.launch(ScanOptions().apply {
@@ -61,6 +71,10 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
                 })
                 true
             }
+            R.id.ok -> {
+                startDownloadProfile()
+                true
+            }
             else -> false
         }
 
@@ -75,4 +89,56 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
             it.setCanceledOnTouchOutside(false)
         }
     }
+
+    private fun startDownloadProfile() {
+        val server = binding.profileDownloadServer.editText!!.let {
+            it.text.toString().trim().apply {
+                if (isEmpty()) {
+                    it.requestFocus()
+                    return@startDownloadProfile
+                }
+            }
+        }
+
+        val code = binding.profileDownloadCode.editText!!.let {
+            it.text.toString().trim().apply {
+                if (isEmpty()) {
+                    it.requestFocus()
+                    return@startDownloadProfile
+                }
+            }
+        }
+
+        downloading = true
+
+        binding.profileDownloadServer.editText!!.isEnabled = false
+        binding.profileDownloadCode.editText!!.isEnabled = false
+
+        binding.progress.isIndeterminate = true
+        binding.progress.visibility = View.VISIBLE
+
+        lifecycleScope.launch {
+            try {
+                doDownloadProfile(server, code)
+            } catch (e: Exception) {
+                Log.d(TAG, "Error downloading profile")
+                Log.d(TAG, Log.getStackTraceString(e))
+                Toast.makeText(context, R.string.profile_download_failed, Toast.LENGTH_LONG).show()
+            } finally {
+                if (parentFragment is EuiccProfilesChangedListener) {
+                    (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
+                }
+                dismiss()
+            }
+        }
+    }
+
+    private suspend fun doDownloadProfile(server: String, code: String) = withContext(Dispatchers.IO) {
+        channel.lpa.downloadProfile("1\$${server}\$${code}", DownloadProgress().apply {
+            setProgressListener { _, _, percentage, _ ->
+                binding.progress.isIndeterminate = false
+                binding.progress.progress = (percentage * 100).toInt()
+            }
+        })
+    }
 }

+ 10 - 0
app/src/main/res/drawable/ic_check_black.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>

+ 21 - 0
app/src/main/res/layout/fragment_profile_download.xml

@@ -16,6 +16,27 @@
         app:layout_constraintWidth_percent="1"
         app:navigationIcon="?homeAsUpIndicator" />
 
+    <View
+        android:id="@+id/guideline"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:orientation="vertical"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toBottomOf="@id/toolbar"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <ProgressBar
+        android:id="@+id/progress"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/guideline"
+        style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
+
     <com.google.android.material.textfield.TextInputLayout
         android:id="@+id/profile_download_server"
         android:layout_width="0dp"

+ 6 - 0
app/src/main/res/menu/fragment_profile_download.xml

@@ -6,4 +6,10 @@
         android:icon="@drawable/ic_scan_black"
         android:title="@string/profile_download_scan"
         app:showAsAction="ifRoom"/>
+
+    <item
+        android:id="@+id/ok"
+        android:icon="@drawable/ic_check_black"
+        android:title="@string/profile_download_ok"
+        app:showAsAction="ifRoom"/>
 </menu>

+ 15 - 0
app/src/main/res/raw/symantec_gsma_rspv2_root_ci1

@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICSTCCAe+gAwIBAgIQbmhWeneg7nyF7hg5Y9+qejAKBggqhkjOPQQDAjBEMRgw
+FgYDVQQKEw9HU00gQXNzb2NpYXRpb24xKDAmBgNVBAMTH0dTTSBBc3NvY2lhdGlv
+biAtIFJTUDIgUm9vdCBDSTEwIBcNMTcwMjIyMDAwMDAwWhgPMjA1MjAyMjEyMzU5
+NTlaMEQxGDAWBgNVBAoTD0dTTSBBc3NvY2lhdGlvbjEoMCYGA1UEAxMfR1NNIEFz
+c29jaWF0aW9uIC0gUlNQMiBSb290IENJMTBZMBMGByqGSM49AgEGCCqGSM49AwEH
+A0IABJ1qutL0HCMX52GJ6/jeibsAqZfULWj/X10p/Min6seZN+hf5llovbCNuB2n
+unLz+O8UD0SUCBUVo8e6n9X1TuajgcAwgb0wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
+EwEB/wQFMAMBAf8wEwYDVR0RBAwwCogIKwYBBAGC6WAwFwYDVR0gAQH/BA0wCzAJ
+BgdngRIBAgEAME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9nc21hLWNybC5zeW1h
+dXRoLmNvbS9vZmZsaW5lY2EvZ3NtYS1yc3AyLXJvb3QtY2kxLmNybDAdBgNVHQ4E
+FgQUgTcPUSXQsdQI1MOyMubSXnlb6/swCgYIKoZIzj0EAwIDSAAwRQIgIJdYsOMF
+WziPK7l8nh5mu0qiRiVf25oa9ullG/OIASwCIQDqCmDrYf+GziHXBOiwJwnBaeBO
+aFsiLzIEOaUuZwdNUw==
+-----END CERTIFICATE-----

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -12,4 +12,6 @@
     <string name="profile_download_server">Server (RSP / SM-DP+)</string>
     <string name="profile_download_code">Activation Code</string>
     <string name="profile_download_scan">Scan QR Code</string>
+    <string name="profile_download_ok">Download</string>
+    <string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>
 </resources>

+ 9 - 0
app/src/main/res/xml/network_security_config.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <base-config>
+        <trust-anchors>
+            <certificates src="@raw/symantec_gsma_rspv2_root_ci1"/>
+            <certificates src="system"/>
+        </trust-anchors>
+    </base-config>
+</network-security-config>