diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f5bee99..f711c5c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,11 @@ + + + + + if (granted) { - startCamera() + // パーミッションが付与されたら onResume でカメラが開始される } else if (!succeedToShowDialog) { PermissionDialog.show(this, CAMERA_PERMISSION_REQUEST_KEY) } else { @@ -63,12 +67,16 @@ class MainActivity : AppCompatActivity() { Settings.get() } private var resultSet: Set = emptySet() + private var isMushroomMode: Boolean = false // Mushroom mode 用フラグ override fun onCreate( savedInstanceState: Bundle?, ) { super.onCreate(savedInstanceState) enableEdgeToEdge() + // Mushroom mode の判定 (intent.action を確認) + isMushroomMode = intent.action?.contains(ACTION_INTERCEPT_MAIN) ?: false + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets -> @@ -87,11 +95,11 @@ class MainActivity : AppCompatActivity() { DividerItemDecoration(this, DividerItemDecoration.VERTICAL), ) vibrator = getSystemService()!! - codeScanner = CodeScanner(this, binding.previewView, ::onDetectCode) + codeScanner = CodeScanner(this, binding.previewView, ::onDetectCode) // CodeScanner はここで初期化 binding.flash.setOnClickListener { codeScanner.toggleTorch() } - codeScanner.getTouchStateStream().observe(this) { + codeScanner.getTouchStateStream().observe(this) { // メソッド名を getTouchStateStream に戻す onFlashOn(it) } detectedPresenter = DetectedPresenter( @@ -116,29 +124,54 @@ class MainActivity : AppCompatActivity() { expandList() } } - if (CameraPermission.hasPermission(this)) { - startCamera() - Updater.startIfAvailable(this) - } else { - launcher.launch() - } PermissionDialog.registerListener(this, CAMERA_PERMISSION_REQUEST_KEY) { finishByError() } OptionsMenuPresenter(this, binding.menu).setUp() + Updater.startIfAvailable(this) // Updaterはパーミッション状態に関わらず呼べるならここに } - override fun onRestart() { - super.onRestart() - if (!started) { - if (CameraPermission.hasPermission(this)) { - startCamera() - } else { - finishByError() + // onStart を追加 + override fun onStart() { + super.onStart() + if (!CameraPermission.hasPermission(this)) { + launcher.launch() + } + // パーミッションがある場合は onResume でカメラが開始される + } + + // onResume を追加/修正 + override fun onResume() { + super.onResume() + if (CameraPermission.hasPermission(this)) { + if (::codeScanner.isInitialized) { // codeScannerが初期化済みか確認 + codeScanner.start() // カメラの初期化/再初期化 + codeScanner.resume() // 解析の再開 } } } + // onPause を追加 + override fun onPause() { + super.onPause() + if (::codeScanner.isInitialized) { // codeScannerが初期化済みか確認 + codeScanner.pause() // 解析の一時停止 + codeScanner.shutdownCamera() // カメラリソースの解放 + } + } + + override fun onRestart() { + super.onRestart() + // 以前の onRestart のロジックは onStart と onResume でカバーされるため、 + // ここでの特別なカメラ処理は不要になる。 + } + + // onDestroy を追加 (任意、デバッグや最終確認用) + override fun onDestroy() { + // onPause で shutdownCamera が呼ばれるため、通常は不要。 + super.onDestroy() + } + private fun finishByError() { toastPermissionError() super.finish() @@ -166,18 +199,12 @@ class MainActivity : AppCompatActivity() { binding.flash.setImageResource(icon) } - private fun startCamera() { - if (started) return - started = true - codeScanner.start() - } - private fun onDetectCode( imageProxy: ImageProxy, codes: List, ) { val detected = mutableListOf() - codes.forEach { + codes.forEach { // it を使用 val value = it.rawValue ?: return@forEach val result = ScanResult( value = value, @@ -186,12 +213,23 @@ class MainActivity : AppCompatActivity() { isUrl = it.valueType == Barcode.TYPE_URL, ) if (!resultSet.contains(result)) { + if (isMushroomMode) { // Mushroom mode の処理を先に行う + val data = Intent() + data.putExtra(REPLACE_KEY_MAIN, result.value) + setResult(RESULT_OK, data) + finish() + return // Mushroomモードでは最初の検出でActivityを終了 + } viewModel.add(result) vibrate() detected.add(it) } } - if (detected.isEmpty()) return + if (detected.isEmpty()) { + // imageProxy.close() は CodeAnalyzer 内で自動的に行われるはずなので、 + // ここでの呼び出しは不要。 + return + } detectedPresenter.onDetected(imageProxy, detected) } diff --git a/app/src/main/kotlin/net/mm2d/codereader/code/CodeScanner.kt b/app/src/main/kotlin/net/mm2d/codereader/code/CodeScanner.kt index e7255a4..f57676a 100644 --- a/app/src/main/kotlin/net/mm2d/codereader/code/CodeScanner.kt +++ b/app/src/main/kotlin/net/mm2d/codereader/code/CodeScanner.kt @@ -14,6 +14,7 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.core.TorchState +import androidx.camera.core.UseCase import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.lifecycle.ProcessCameraProvider @@ -40,13 +41,20 @@ class CodeScanner( private val analyzer: CodeAnalyzer = CodeAnalyzer(scanner, callback) private var camera: Camera? = null private val torchStateFlow: MutableStateFlow = MutableStateFlow(false) + + private var cameraProviderInternal: ProcessCameraProvider? = null + private var previewUseCaseInternal: Preview? = null + private var analysisUseCaseInternal: ImageAnalysis? = null + fun getTouchStateStream(): Flow = torchStateFlow init { activity.lifecycle.addObserver( LifecycleEventObserver { _, event -> if (event == Event.ON_DESTROY) { - workerExecutor.shutdown() + if (!workerExecutor.isShutdown) { + workerExecutor.shutdown() + } scanner.close() } }, @@ -56,7 +64,13 @@ class CodeScanner( fun start() { val future = ProcessCameraProvider.getInstance(activity) future.addListener({ - setUp(future.get()) + try { + val provider = future.get() + this.cameraProviderInternal = provider + setUp(provider) + } catch (e: Exception) { + Timber.e(e, "CodeScanner: Failed to get ProcessCameraProvider in start() listener.") + } }, ContextCompat.getMainExecutor(activity)) } @@ -66,36 +80,59 @@ class CodeScanner( val resolutionSelector = ResolutionSelector.Builder() .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY) .build() + val preview = Preview.Builder() .setResolutionSelector(resolutionSelector) .build() preview.surfaceProvider = previewView.surfaceProvider + this.previewUseCaseInternal = preview val analysis = ImageAnalysis.Builder() .setResolutionSelector(resolutionSelector) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(workerExecutor, analyzer) + this.analysisUseCaseInternal = analysis try { provider.unbindAll() val camera = provider.bindToLifecycle( activity, CameraSelector.DEFAULT_BACK_CAMERA, - preview, - analysis, + previewUseCaseInternal!!, + analysisUseCaseInternal!!, ) + this.camera = camera camera.cameraInfo.torchState.observe(activity) { state -> torchStateFlow.tryEmit(state == TorchState.ON) } - this.camera = camera } catch (e: Exception) { - Timber.e(e) + Timber.e(e, "CodeScanner: Use case binding failed in setUp.") + this.camera = null + } + } + + fun shutdownCamera() { + cameraProviderInternal?.let { provider -> + val useCasesToUnbind = mutableListOf() + previewUseCaseInternal?.let { useCasesToUnbind.add(it) } + analysisUseCaseInternal?.let { useCasesToUnbind.add(it) } + + if (useCasesToUnbind.isNotEmpty()) { + try { + provider.unbind(*useCasesToUnbind.toTypedArray()) + } catch (e: Exception) { + Timber.e(e, "CodeScanner: [shutdownCamera] Error unbinding specific use cases: ${e.message}") + } + } } + previewUseCaseInternal = null + analysisUseCaseInternal = null + camera = null } fun toggleTorch() { - val camera = camera ?: return + val camera = this.camera ?: return camera.cameraControl.enableTorch(!torchStateFlow.value) }