Unverified Commit 63e6e604 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #2551 from Mygod/mlkit

Migrate to ML kit for scanning QR code
parents 73fa7c97 0828a7e0
......@@ -71,7 +71,7 @@ dependencies {
val workVersion = "2.4.0-rc01"
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-livedata-core-ktx:$lifecycleVersion")
api("androidx.preference:preference:1.1.1")
......
......@@ -44,7 +44,7 @@ import java.net.InetAddress
import kotlin.coroutines.resume
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
for (element in this) try {
action(element)
......
......@@ -12,14 +12,18 @@ setupApp()
android.defaultConfig.applicationId = "com.github.shadowsocks"
dependencies {
val cameraxVersion = "1.0.0-beta06"
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.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.mlkit:barcode-scanning:16.0.0")
implementation("com.google.zxing:core:3.4.0")
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.4")
implementation("me.zhanghai.android.fastscroll:library:1.1.4")
implementation("xyz.belvi.mobilevision:barcodescanner:2.0.3")
}
......@@ -56,6 +56,8 @@
android:name=".ScannerActivity"
android:label="@string/add_profile_methods_scan_qr_code"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Shadowsocks.Immersive"
android:screenOrientation="locked"
android:excludeFromRecents="true"/>
<activity android:name=".tasker.ConfigActivity"
......
......@@ -20,98 +20,110 @@
package com.github.shadowsocks
import android.Manifest
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.ShortcutManager
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.SparseArray
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
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.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.ProfileManager
import com.github.shadowsocks.utils.datas
import com.github.shadowsocks.utils.forEachTry
import com.github.shadowsocks.utils.openBitmap
import com.github.shadowsocks.utils.readableMessage
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture
import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic
import com.google.android.gms.vision.Frame
import com.google.android.gms.vision.barcode.Barcode
import com.google.android.gms.vision.barcode.BarcodeDetector
import com.github.shadowsocks.widget.ListHolderListener
import com.google.mlkit.vision.barcode.Barcode
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import kotlinx.coroutines.*
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import xyz.belvi.mobilevisionbarcodescanner.BarcodeRetriever
class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
class ScannerActivity : AppCompatActivity(), ImageAnalysis.Analyzer {
companion object {
private const val REQUEST_IMPORT = 2
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() {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(
"market://details?id=com.github.sumimakito.awesomeqrsample")))
} catch (_: ActivityNotFoundException) { }
finish()
@ExperimentalGetImage
override fun analyze(image: ImageProxy) {
val mediaImage = image.image ?: return
lifecycleScope.launchWhenCreated {
val result = try {
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?) {
super.onCreate(savedInstanceState)
detector = BarcodeDetector.Builder(this)
.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 < 23) return startImport() // we show no love to lollipop
if (Build.VERSION.SDK_INT >= 25) getSystemService<ShortcutManager>()!!.reportShortcutUsed("scan")
if (try {
getSystemService<CameraManager>()?.cameraIdList?.isEmpty()
} catch (_: CameraAccessException) {
true
} != false) {
startImport()
return
}
} != false) return startImport()
setContentView(R.layout.layout_scanner)
ListHolderListener.setup(this)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
val capture = supportFragmentManager.findFragmentById(R.id.barcode) as BarcodeCapture
capture.setCustomDetector(detector)
capture.setRetrieval(this)
requestCamera.launch(Manifest.permission.CAMERA)
}
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 {
Profile.findAllUrls(barcode.rawValue, Core.currentProfile?.main).forEach { ProfileManager.createProfile(it) }
onSupportNavigateUp()
private suspend inline fun process(feature: Profile? = Core.currentProfile?.main,
crossinline image: () -> InputImage): Boolean {
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
}
return result
}
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false)
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()
startImport()
}
......@@ -133,6 +145,13 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
*/
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 {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
......@@ -140,26 +159,23 @@ class ScannerActivity : AppCompatActivity(), BarcodeRetriever {
}, if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
val feature = Core.currentProfile?.main
try {
var success = false
data!!.datas.forEachTry { uri ->
detector.detect(Frame.Builder().setBitmap(contentResolver.openBitmap(uri)).build())
.forEach { _, barcode ->
Profile.findAllUrls(barcode.rawValue, feature).forEach {
ProfileManager.createProfile(it)
success = true
}
}
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> when {
resultCode == Activity.RESULT_OK -> GlobalScope.launch(Dispatchers.Main.immediate) {
onSupportNavigateUp()
val feature = Core.currentProfile?.main
try {
var success = false
data!!.datas.forEachTry { uri ->
if (process(feature) { InputImage.fromFilePath(app, uri) }) success = true
}
Toast.makeText(app, if (success) R.string.action_import_msg else R.string.action_import_err,
Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()
}
Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err,
Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(this, e.readableMessage, Toast.LENGTH_LONG).show()
}
onSupportNavigateUp()
} else if (requestCode == REQUEST_IMPORT_OR_FINISH) onSupportNavigateUp()
requestCode == REQUEST_IMPORT_OR_FINISH -> onSupportNavigateUp()
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
......
......@@ -33,7 +33,7 @@ object ListHolderListener : View.OnApplyWindowInsetsListener {
return insets.replaceSystemWindowInsets(0, 0, 0, insets.systemWindowInsetBottom)
}
fun setup(activity: AppCompatActivity) = activity.findViewById<View>(android.R.id.content).apply {
fun setup(activity: AppCompatActivity) = activity.findViewById<View>(android.R.id.content).run {
setOnApplyWindowInsetsListener(ListHolderListener)
systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar_light_dark" />
<fragment
<androidx.camera.view.PreviewView
android:id="@+id/barcode"
android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
app:gvb_auto_focus="true"/>
android:layout_height="fill_parent"/>
</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