QRScannerActivity.kt 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2021-2024 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.camera
  22. import android.annotation.SuppressLint
  23. import android.content.Intent
  24. import android.graphics.Color
  25. import android.os.Build
  26. import android.os.Bundle
  27. import android.util.Size
  28. import android.view.MotionEvent
  29. import android.view.View
  30. import android.view.WindowManager
  31. import android.widget.ImageView
  32. import android.widget.TextView
  33. import android.widget.Toast
  34. import androidx.camera.core.Camera
  35. import androidx.camera.core.CameraSelector
  36. import androidx.camera.core.FocusMeteringAction
  37. import androidx.camera.core.ImageAnalysis
  38. import androidx.camera.core.ImageCapture
  39. import androidx.camera.core.Preview
  40. import androidx.camera.lifecycle.ProcessCameraProvider
  41. import androidx.camera.view.PreviewView
  42. import androidx.core.content.ContextCompat
  43. import ch.threema.app.R
  44. import ch.threema.app.ThreemaApplication
  45. import ch.threema.app.activities.ThreemaActivity
  46. import ch.threema.app.services.QRCodeServiceImpl.QRCodeColor
  47. import ch.threema.app.services.QRCodeServiceImpl.QR_TYPE_ANY
  48. import ch.threema.app.utils.SoundUtil
  49. import ch.threema.base.utils.LoggingUtil
  50. import java.util.concurrent.ExecutorService
  51. import java.util.concurrent.Executors
  52. class QRScannerActivity : ThreemaActivity() {
  53. companion object {
  54. const val KEY_HINT_TEXT: String = "hint"
  55. const val KEY_QR_TYPE: String = "qrType"
  56. }
  57. private val logger = LoggingUtil.getThreemaLogger("QRScannerActivity")
  58. private lateinit var cameraExecutor: ExecutorService
  59. private lateinit var cameraPreview: PreviewView
  60. private lateinit var cameraPreviewContainer: View
  61. private var hint: String = ""
  62. @QRCodeColor private var qrColor = QR_TYPE_ANY
  63. private var camera: Camera? = null
  64. private var preview: Preview? = null
  65. private var imageCapture: ImageCapture? = null
  66. private var imageAnalyzer: ImageAnalysis? = null
  67. private var cameraProvider: ProcessCameraProvider? = null
  68. override fun onCreate(savedInstanceState: Bundle?) {
  69. super.onCreate(savedInstanceState)
  70. cameraExecutor = Executors.newSingleThreadExecutor()
  71. setContentView(R.layout.activity_qrscanner)
  72. cameraPreview = findViewById(R.id.camera_preview)
  73. cameraPreviewContainer = findViewById(R.id.camera_preview_container)
  74. window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
  75. window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
  76. window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
  77. window.statusBarColor = Color.TRANSPARENT
  78. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  79. // we want dark icons, i.e. a light status bar
  80. window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
  81. }
  82. qrColor = intent.getIntExtra(KEY_QR_TYPE, QR_TYPE_ANY)
  83. if (hint.isEmpty()) {
  84. hint = if (intent.hasExtra(KEY_HINT_TEXT)) {
  85. intent.getStringExtra(KEY_HINT_TEXT)!!
  86. } else {
  87. getString(R.string.msg_default_status)
  88. }
  89. }
  90. // set hint text
  91. findViewById<TextView>(R.id.hint_view)?.let {
  92. it.text = hint
  93. }
  94. // set viewfinder color
  95. findViewById<ImageView>(R.id.camera_viewfinder)?.let {
  96. if (qrColor == QR_TYPE_ANY) {
  97. it.visibility = View.GONE
  98. } else {
  99. it.setColorFilter(qrColor)
  100. }
  101. }
  102. // Wait for the views to be properly laid out
  103. cameraPreview.post {
  104. setUpCamera()
  105. }
  106. }
  107. override fun onDestroy() {
  108. super.onDestroy()
  109. // Shut down our background executor
  110. cameraExecutor.shutdown()
  111. }
  112. private fun setUpCamera() {
  113. val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  114. cameraProviderFuture.addListener({
  115. // CameraProvider
  116. cameraProvider = cameraProviderFuture.get()
  117. // Build and bind the camera use cases
  118. bindCameraUseCases()
  119. }, ContextCompat.getMainExecutor(this))
  120. }
  121. @SuppressLint("ClickableViewAccessibility", "WrongConstant")
  122. private fun bindCameraUseCases() {
  123. val lensFacing = if (cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) == true) CameraSelector.LENS_FACING_BACK else
  124. (if (cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) == true) CameraSelector.LENS_FACING_FRONT else -1)
  125. if (lensFacing == -1) {
  126. Toast.makeText(this, R.string.no_camera_installed, Toast.LENGTH_SHORT).show()
  127. logger.info("Back and front camera are unavailable")
  128. finish()
  129. return
  130. }
  131. val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
  132. val rotation = cameraPreview.display?.rotation ?: 0
  133. val resolution = Size(720, 1280)
  134. val cameraProvider = cameraProvider
  135. ?: throw IllegalStateException("Camera initialization failed.")
  136. preview = Preview.Builder()
  137. .setTargetResolution(resolution)
  138. .setTargetRotation(rotation)
  139. .build()
  140. imageCapture = ImageCapture.Builder()
  141. .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
  142. .setTargetResolution(resolution)
  143. .setTargetRotation(rotation)
  144. .build()
  145. imageAnalyzer = ImageAnalysis.Builder()
  146. .setTargetResolution(resolution)
  147. .setTargetRotation(rotation)
  148. .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
  149. .build()
  150. .also {
  151. it.setAnalyzer(cameraExecutor, QRCodeAnalyzer { decodeQRCodeState: DecodeQRCodeState ->
  152. when (decodeQRCodeState) {
  153. is DecodeQRCodeState.ERROR -> {
  154. logger.debug("Decoder Error")
  155. it.clearAnalyzer()
  156. runOnUiThread {
  157. Toast.makeText(this, R.string.qr_code, Toast.LENGTH_LONG).show()
  158. returnData(null, false)
  159. }
  160. }
  161. is DecodeQRCodeState.SUCCESS -> {
  162. logger.debug("Decoder Success")
  163. val qrCodeData = decodeQRCodeState.qrCode
  164. it.clearAnalyzer()
  165. qrCodeData?.let {
  166. returnData(qrCodeData, true)
  167. }
  168. }
  169. }
  170. })
  171. }
  172. try {
  173. // Must unbind the use-cases before rebinding them
  174. cameraProvider.unbindAll()
  175. camera = cameraProvider.bindToLifecycle(
  176. this, cameraSelector, preview, imageCapture, imageAnalyzer)
  177. // Attach the viewfinder's surface provider to preview use case
  178. preview?.setSurfaceProvider(cameraPreview.surfaceProvider)
  179. val point = cameraPreview.meteringPointFactory.createPoint(
  180. cameraPreviewContainer.left + cameraPreviewContainer.width / 2.0f,
  181. cameraPreviewContainer.top + cameraPreviewContainer.height / 2.0f)
  182. camera?.cameraControl?.startFocusAndMetering(FocusMeteringAction.Builder(point).build())
  183. } catch (e: Exception) {
  184. logger.error("Use case binding failed", e)
  185. returnData(null, false)
  186. }
  187. cameraPreviewContainer.setOnTouchListener { _: View, motionEvent: MotionEvent ->
  188. when (motionEvent.action) {
  189. MotionEvent.ACTION_DOWN -> true
  190. MotionEvent.ACTION_UP -> {
  191. camera?.cameraControl?.startFocusAndMetering(
  192. FocusMeteringAction.Builder(
  193. cameraPreview.meteringPointFactory.createPoint(
  194. motionEvent.x, motionEvent.y
  195. )
  196. ).build()
  197. )
  198. true
  199. }
  200. else -> false
  201. }
  202. }
  203. }
  204. private fun returnData(qrCodeData: String?, success: Boolean) {
  205. if (success) {
  206. SoundUtil.play(R.raw.qrscanner_beep)
  207. val intent = Intent()
  208. intent.putExtra(ThreemaApplication.INTENT_DATA_QRCODE, qrCodeData)
  209. setResult(RESULT_OK, intent)
  210. } else {
  211. setResult(RESULT_CANCELED)
  212. }
  213. finish()
  214. }
  215. }