Browse Source

feat: Scan QR code from gallery

Close #6.
Peter Cai 1 year ago
parent
commit
fc4e5739de

+ 35 - 4
app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt

@@ -3,6 +3,7 @@ package im.angry.openeuicc.ui
 import android.annotation.SuppressLint
 import android.app.Dialog
 import android.content.DialogInterface
+import android.graphics.BitmapFactory
 import android.os.Bundle
 import android.text.Editable
 import android.util.Log
@@ -10,6 +11,7 @@ import android.view.*
 import android.widget.ProgressBar
 import android.widget.TextView
 import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.widget.Toolbar
 import androidx.lifecycle.lifecycleScope
 import com.google.android.material.textfield.TextInputLayout
@@ -54,13 +56,38 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
     private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
         result.contents?.let { content ->
             Log.d(TAG, content)
-            val components = content.split("$")
-            if (components.size < 3 || components[0] != "LPA:1") return@registerForActivityResult
-            profileDownloadServer.editText?.setText(components[1])
-            profileDownloadCode.editText?.setText(components[2])
+            onScanResult(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) {
+                            onScanResult(it)
+                        }
+                    }
+
+                    bmp.recycle()
+                }
+            }
+        }
+    }
+
+    private fun onScanResult(result: String) {
+        val components = result.split("$")
+        if (components.size < 3 || components[0] != "LPA:1") return
+        profileDownloadServer.editText?.setText(components[1])
+        profileDownloadCode.editText?.setText(components[2])
+    }
+
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -103,6 +130,10 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
                 })
                 true
             }
+            R.id.scan_from_gallery -> {
+                gallerySelectorLauncher.launch("image/*")
+                true
+            }
             R.id.ok -> {
                 startDownloadProfile()
                 true

+ 16 - 0
app-common/src/main/java/im/angry/openeuicc/util/Utils.kt

@@ -2,9 +2,14 @@ package im.angry.openeuicc.util
 
 import android.content.Context
 import android.content.pm.PackageManager
+import android.graphics.Bitmap
 import android.se.omapi.SEService
 import android.telephony.TelephonyManager
 import androidx.fragment.app.Fragment
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.RGBLuminanceSource
+import com.google.zxing.common.HybridBinarizer
+import com.google.zxing.qrcode.QRCodeReader
 import im.angry.openeuicc.OpenEuiccApplication
 import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.di.AppContainer
@@ -88,3 +93,14 @@ suspend fun connectSEService(context: Context): SEService = suspendCoroutine { c
         }
     }
 }
+
+fun decodeQrFromBitmap(bmp: Bitmap): String? =
+     runCatching {
+        val pixels = IntArray(bmp.width * bmp.height)
+        bmp.getPixels(pixels, 0, bmp.width, 0, 0, bmp.width, bmp.height)
+
+        val luminanceSource = RGBLuminanceSource(bmp.width, bmp.height, pixels)
+        val binaryBmp = BinaryBitmap(HybridBinarizer(luminanceSource))
+
+        QRCodeReader().decode(binaryBmp).text
+    }.getOrNull()

+ 5 - 0
app-common/src/main/res/drawable/ic_gallery_black.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"/>
+</vector>

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

@@ -7,6 +7,12 @@
         android:title="@string/profile_download_scan"
         app:showAsAction="ifRoom"/>
 
+    <item
+        android:id="@+id/scan_from_gallery"
+        android:icon="@drawable/ic_gallery_black"
+        android:title="@string/profile_download_scan_from_gallery"
+        app:showAsAction="ifRoom" />
+
     <item
         android:id="@+id/ok"
         android:icon="@drawable/ic_check_black"

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

@@ -31,6 +31,7 @@
     <string name="profile_download_imei">IMEI (Optional)</string>
     <string name="profile_download_free_space">Space remaining: %s</string>
     <string name="profile_download_scan">Scan QR Code</string>
+    <string name="profile_download_scan_from_gallery">Scan QR Code from Gallery</string>
     <string name="profile_download_ok">Download</string>
     <string name="profile_download_failed">Failed to download eSIM. Check your activation / QR code.</string>