____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt
/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.ui
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ShortcutManager
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.NotFoundException
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.GlobalHistogramBinarizer
import com.google.zxing.qrcode.QRCodeReader
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProfileManager
import io.nekohasekai.sagernet.databinding.LayoutScannerBinding
import io.nekohasekai.sagernet.group.RawUpdater
import io.nekohasekai.sagernet.ktx.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import androidx.core.net.toUri
import androidx.room.util.query
import io.nekohasekai.sagernet.utils.ZxingQRCodeAnalyzer
import libexclavecore.Libexclavecore
import java.net.URI
class ScannerActivity : ThemedActivity() {
private lateinit var analysisExecutor: ExecutorService
private lateinit var binding: LayoutScannerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 25) getSystemService()!!.reportShortcutUsed("scan")
binding = LayoutScannerBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.apply {
setTitle(R.string.add_profile_methods_scan_qr_code)
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_navigation_close)
}
analysisExecutor = Executors.newSingleThreadExecutor()
binding.previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
binding.previewView.previewStreamState.observe(this) {
if (it === PreviewView.StreamState.STREAMING) {
binding.previewView.implementationMode = PreviewView.ImplementationMode.PERFORMANCE
}
}
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
startCamera()
} else {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
startCamera()
} else {
setResult(RESULT_CANCELED)
finish()
}
}
private lateinit var imageAnalysis: ImageAnalysis
private lateinit var imageAnalyzer: ImageAnalysis.Analyzer
private val onSuccess: (String) -> Unit = { rawValue: String ->
imageAnalysis.clearAnalyzer()
onSuccess(rawValue)
}
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var cameraPreview: Preview
private lateinit var camera: Camera
private fun startCamera() {
val cameraProviderFuture = try {
ProcessCameraProvider.getInstance(this)
} catch (e: Exception) {
fatalError(e)
return
}
cameraProviderFuture.addListener({
cameraProvider = try {
cameraProviderFuture.get()
} catch (e: Exception) {
fatalError(e)
return@addListener
}
cameraPreview = Preview.Builder().build()
.also { it.surfaceProvider = binding.previewView.surfaceProvider }
imageAnalysis = ImageAnalysis.Builder().build()
imageAnalyzer = ZxingQRCodeAnalyzer(onSuccess, fatalError)
imageAnalysis.setAnalyzer(analysisExecutor, imageAnalyzer)
cameraProvider.unbindAll()
try {
camera = cameraProvider.bindToLifecycle(
this, CameraSelector.DEFAULT_BACK_CAMERA, cameraPreview, imageAnalysis
)
} catch (e: Exception) {
fatalError(e)
}
}, ContextCompat.getMainExecutor(this))
}
private val fatalError: (Exception?) -> Unit = {
if (it != null) Logs.w(it)
runOnMainDispatcher {
Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
private fun onSuccess(value: String): Boolean {
finish()
runOnDefaultDispatcher {
try {
val results = RawUpdater.parseRaw(value)
if (results.isNullOrEmpty()) {
if (!value.contains("\n") && !value.contains("\r") && isHTTPorHTTPSURL(value)) {
val uri = Uri.Builder()
.scheme("owenclave")
.authority("subscription")
.appendQueryParameter("url", value)
.build()
startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = uri
})
} else {
fatalError(null)
}
} else {
val currentGroupId = DataStore.selectedGroupForImport()
if (DataStore.selectedGroup != currentGroupId) {
DataStore.selectedGroup = currentGroupId
}
for (profile in results) {
ProfileManager.createProfile(currentGroupId, profile)
}
}
} catch (e: Exception) {
fatalError(e)
}
}
return true
}
private val importCodeFile =
registerForActivityResult(ActivityResultContracts.GetMultipleContents()) {
runOnDefaultDispatcher {
try {
it.forEachTry { uri ->
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(
ImageDecoder.createSource(
contentResolver, uri
)
) { decoder, _, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
decoder.isMutableRequired = true
}
} else {
@Suppress("DEPRECATION") MediaStore.Images.Media.getBitmap(
contentResolver, uri
)
}
val intArray = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(
intArray,
0,
bitmap.width,
0,
0,
bitmap.width,
bitmap.height
)
val source = RGBLuminanceSource(bitmap.width, bitmap.height, intArray)
val qrReader = QRCodeReader()
try {
val result = try {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source)),
mapOf(DecodeHintType.TRY_HARDER to true)
)
} catch (_: NotFoundException) {
qrReader.decode(
BinaryBitmap(GlobalHistogramBinarizer(source.invert())),
mapOf(DecodeHintType.TRY_HARDER to true)
)
}
val results = RawUpdater.parseRaw(result.text ?: "")
if (!results.isNullOrEmpty()) {
onMainDispatcher {
finish()
runOnDefaultDispatcher {
val currentGroupId = DataStore.selectedGroupForImport()
if (DataStore.selectedGroup != currentGroupId) {
DataStore.selectedGroup = currentGroupId
}
for (profile in results) {
ProfileManager.createProfile(currentGroupId, profile)
}
}
}
} else {
if (!result.text.contains("\n") && !result.text.contains("\r") && isHTTPorHTTPSURL(result.text)) {
val uri = Uri.Builder()
.scheme("owenclave")
.authority("subscription")
.appendQueryParameter("url", result.text)
.build()
startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = uri
})
finish()
} else {
Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
} catch (e: Throwable) {
Logs.w(e)
onMainDispatcher {
Toast.makeText(app, R.string.action_import_err, Toast.LENGTH_SHORT).show()
}
}
}
} catch (e: Exception) {
Logs.w(e)
onMainDispatcher {
Toast.makeText(app, e.readableMessage, Toast.LENGTH_LONG).show()
}
}
}
}
/**
* See also: https://stackoverflow.com/a/31350642/2245107
*/
override fun shouldUpRecreateTask(targetIntent: Intent?): Boolean {
return super.shouldUpRecreateTask(targetIntent) || isTaskRoot
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return binding.previewView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.scanner_menu, menu)
return true
}
// Every time camera.cameraInfo.cameraSelector will get different value,
// so useFront used to record it.
private var useFront = false
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_import_file -> {
startFilesForResult(importCodeFile, "image/*")
}
R.id.action_flash -> {
if (!::camera.isInitialized) {
return true
}
val torchOn = camera.cameraInfo.torchState.value == TorchState.ON
camera.cameraControl.enableTorch(!torchOn)
}
R.id.action_camera_switch -> {
if (!::camera.isInitialized) {
return true
}
useFront = !useFront
camera.cameraControl.enableTorch(false)
val cameraSelector = if (useFront) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
cameraProvider.unbindAll()
try {
camera = cameraProvider.bindToLifecycle(
this,
cameraSelector,
cameraPreview,
imageAnalysis
)
} catch (e: Exception) {
fatalError(e)
}
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
}