Commit bbc635b1 authored by Mygod's avatar Mygod

Migrate to ML kit for scanning QR code

Fixes #2548.
parent 9d2bc2d6
...@@ -71,7 +71,7 @@ dependencies { ...@@ -71,7 +71,7 @@ dependencies {
val workVersion = "2.4.0-rc01" val workVersion = "2.4.0-rc01"
api(project(":plugin")) api(project(":plugin"))
api("androidx.fragment:fragment-ktx:1.2.5") api("androidx.fragment:fragment-ktx:1.3.0-alpha06")
api("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") api("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
api("androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycleVersion") api("androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycleVersion")
api("androidx.preference:preference:1.1.1") api("androidx.preference:preference:1.1.1")
......
...@@ -44,7 +44,7 @@ import java.net.InetAddress ...@@ -44,7 +44,7 @@ import java.net.InetAddress
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) { inline fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) {
var result: Exception? = null var result: Exception? = null
for (element in this) try { for (element in this) try {
action(element) action(element)
......
...@@ -12,14 +12,18 @@ setupApp() ...@@ -12,14 +12,18 @@ setupApp()
android.defaultConfig.applicationId = "com.github.shadowsocks" android.defaultConfig.applicationId = "com.github.shadowsocks"
dependencies { dependencies {
val cameraxVersion = "1.0.0-beta06"
implementation("androidx.browser:browser:1.2.0") implementation("androidx.browser:browser:1.2.0")
implementation("androidx.camera:camera-camera2:$cameraxVersion")
implementation("androidx.camera:camera-lifecycle:$cameraxVersion")
implementation("androidx.camera:camera-view:1.0.0-alpha13")
implementation("androidx.constraintlayout:constraintlayout:2.0.0-beta7") implementation("androidx.constraintlayout:constraintlayout:2.0.0-beta7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation("com.google.android.gms:play-services-vision:20.1.0")
implementation("com.google.firebase:firebase-ads:19.2.0") implementation("com.google.firebase:firebase-ads:19.2.0")
implementation("com.google.mlkit:barcode-scanning:16.0.0")
implementation("com.google.zxing:core:3.4.0") implementation("com.google.zxing:core:3.4.0")
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0") implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.4") implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.4")
implementation("me.zhanghai.android.fastscroll:library:1.1.4") implementation("me.zhanghai.android.fastscroll:library:1.1.4")
implementation("xyz.belvi.mobilevision:barcodescanner:2.0.3")
} }
...@@ -20,98 +20,108 @@ ...@@ -20,98 +20,108 @@
package com.github.shadowsocks package com.github.shadowsocks
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager
import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.SparseArray
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.util.forEach import androidx.lifecycle.lifecycleScope
import androidx.work.await
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.utils.datas import com.github.shadowsocks.utils.datas
import com.github.shadowsocks.utils.forEachTry import com.github.shadowsocks.utils.forEachTry
import com.github.shadowsocks.utils.openBitmap
import com.github.shadowsocks.utils.readableMessage import com.github.shadowsocks.utils.readableMessage
import com.google.android.gms.common.GoogleApiAvailability import com.google.mlkit.vision.barcode.Barcode
import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.android.gms.vision.Frame import com.google.mlkit.vision.common.InputImage
import com.google.android.gms.vision.barcode.Barcode import kotlinx.coroutines.*
import com.google.android.gms.vision.barcode.BarcodeDetector import kotlinx.coroutines.tasks.await
import timber.log.Timber import timber.log.Timber
import xyz.belvi.mobilevisionbarcodescanner.BarcodeRetriever
class ScannerActivity : AppCompatActivity(), BarcodeRetriever { class ScannerActivity : AppCompatActivity(), ImageAnalysis.Analyzer {
companion object { companion object {
private const val REQUEST_IMPORT = 2 private const val REQUEST_IMPORT = 2
private const val REQUEST_IMPORT_OR_FINISH = 3 private const val REQUEST_IMPORT_OR_FINISH = 3
private const val REQUEST_GOOGLE_API = 4
} }
private lateinit var detector: BarcodeDetector private val scanner = BarcodeScanning.getClient(BarcodeScannerOptions.Builder().apply {
setBarcodeFormats(Barcode.FORMAT_QR_CODE)
}.build())
private val imageAnalysis by lazy {
ImageAnalysis.Builder().apply {
setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
}.build().also { it.setAnalyzer(Dispatchers.Main.immediate.asExecutor(), this) }
}
private fun fallback() { @ExperimentalGetImage
try { override fun analyze(image: ImageProxy) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse( val mediaImage = image.image ?: return
"market://details?id=com.github.sumimakito.awesomeqrsample"))) lifecycleScope.launchWhenCreated {
} catch (_: ActivityNotFoundException) { } val result = try {
finish() process { InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees) }.also {
if (it) imageAnalysis.clearAnalyzer()
}
} catch (e: Exception) {
return@launchWhenCreated Timber.w(e)
} finally {
image.close()
}
if (result) onSupportNavigateUp()
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
detector = BarcodeDetector.Builder(this) if (Build.VERSION.SDK_INT < 23) return startImport() // we show no love to lollipop
.setBarcodeFormats(Barcode.QR_CODE)
.build()
if (!detector.isOperational) {
val availability = GoogleApiAvailability.getInstance()
val dialog = availability.getErrorDialog(this, availability.isGooglePlayServicesAvailable(this),
REQUEST_GOOGLE_API)
if (dialog == null) {
Toast.makeText(this, R.string.common_google_play_services_notification_ticker, Toast.LENGTH_SHORT)
.show()
fallback()
} else {
dialog.setOnDismissListener { fallback() }
dialog.show()
}
return
}
if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed("scan") if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed("scan")
if (try { if (try {
getSystemService<CameraManager>()?.cameraIdList?.isEmpty() getSystemService<CameraManager>()?.cameraIdList?.isEmpty()
} catch (_: CameraAccessException) { } catch (_: CameraAccessException) {
true true
} != false) { } != false) return startImport()
startImport()
return
}
setContentView(R.layout.layout_scanner) setContentView(R.layout.layout_scanner)
setSupportActionBar(findViewById(R.id.toolbar)) setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
val capture = supportFragmentManager.findFragmentById(R.id.barcode) as BarcodeCapture requestCamera.launch(Manifest.permission.CAMERA)
capture.setCustomDetector(detector) }
capture.setRetrieval(this) private val requestCamera = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) lifecycleScope.launchWhenCreated {
val cameraProvider = ProcessCameraProvider.getInstance(this@ScannerActivity).await()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(findViewById<PreviewView>(R.id.barcode).createSurfaceProvider())
cameraProvider.bindToLifecycle(this@ScannerActivity, CameraSelector.Builder().apply {
requireLensFacing(CameraSelector.LENS_FACING_BACK)
}.build(), preview, imageAnalysis)
} else permissionMissing()
} }
override fun onRetrieved(barcode: Barcode) = runOnUiThread { private suspend inline fun process(feature: Profile? = Core.currentProfile?.main,
Profile.findAllUrls(barcode.rawValue, Core.currentProfile?.main).forEach { ProfileManager.createProfile(it) } crossinline image: () -> InputImage): Boolean {
onSupportNavigateUp() val barcodes = withContext(Dispatchers.Default) { scanner.process(image()).await() }
var result = false
for (profile in Profile.findAllUrls(barcodes.mapNotNull { it.rawValue }.joinToString("\n"), feature)) {
ProfileManager.createProfile(profile)
result = true
} }
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false) return result
override fun onBitmapScanned(sparseArray: SparseArray<Barcode>?) { } }
override fun onRetrievedFailed(reason: String?) = Timber.w(reason)
override fun onPermissionRequestDenied() { private fun permissionMissing() {
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
startImport() startImport()
} }
...@@ -133,6 +143,13 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever { ...@@ -133,6 +143,13 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
*/ */
override fun shouldUpRecreateTask(targetIntent: Intent?) = super.shouldUpRecreateTask(targetIntent) || isTaskRoot override fun shouldUpRecreateTask(targetIntent: Intent?) = super.shouldUpRecreateTask(targetIntent) || isTaskRoot
private var finished = false
override fun onSupportNavigateUp(): Boolean {
if (finished) return false
finished = true
return super.onSupportNavigateUp()
}
private fun startImport(manual: Boolean = false) = startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply { private fun startImport(manual: Boolean = false) = startActivityForResult(Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*" type = "image/*"
...@@ -140,26 +157,23 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever { ...@@ -140,26 +157,23 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
}, if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH) }, if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) { REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> when {
resultCode == Activity.RESULT_OK -> GlobalScope.launch(Dispatchers.Main.immediate) {
onSupportNavigateUp()
val feature = Core.currentProfile?.main val feature = Core.currentProfile?.main
try { try {
var success = false var success = false
data!!.datas.forEachTry { uri -> data!!.datas.forEachTry { uri ->
detector.detect(Frame.Builder().setBitmap(contentResolver.openBitmap(uri)).build()) if (process(feature) { InputImage.fromFilePath(app, uri) }) success = true
.forEach { _, barcode ->
Profile.findAllUrls(barcode.rawValue, feature).forEach {
ProfileManager.createProfile(it)
success = true
} }
} Toast.makeText(app, if (success) R.string.action_import_msg else R.string.action_import_err,
}
Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err,
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(this, e.readableMessage, Toast.LENGTH_LONG).show() Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()
}
}
requestCode == REQUEST_IMPORT_OR_FINISH -> onSupportNavigateUp()
} }
onSupportNavigateUp()
} else if (requestCode == REQUEST_IMPORT_OR_FINISH) onSupportNavigateUp()
else -> super.onActivityResult(requestCode, resultCode, data) else -> super.onActivityResult(requestCode, resultCode, data)
} }
} }
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" /> <include layout="@layout/toolbar_light_dark" />
<fragment <androidx.camera.view.PreviewView
android:id="@+id/barcode" android:id="@+id/barcode"
android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"/>
app:gvb_auto_focus="true"/>
</LinearLayout> </LinearLayout>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment