Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.adamrocker.android.simeji.ACTION_INTERCEPT" />
<category android:name="com.adamrocker.android.simeji.REPLACE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>

<activity
Expand Down
88 changes: 63 additions & 25 deletions app/src/main/kotlin/net/mm2d/codereader/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package net.mm2d.codereader

import android.animation.ValueAnimator
import android.content.Intent // Mushroom mode 用に追加
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
Expand Down Expand Up @@ -42,13 +43,16 @@ import net.mm2d.codereader.util.ReviewRequester
import net.mm2d.codereader.util.Updater
import net.mm2d.codereader.util.observe

// Mushroom mode 用に追加
const val ACTION_INTERCEPT_MAIN = "com.adamrocker.android.simeji.ACTION_INTERCEPT" // 定数名変更
const val REPLACE_KEY_MAIN = "replace_key" // 定数名変更

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var codeScanner: CodeScanner
private var started: Boolean = false
private val launcher = registerForCameraPermissionRequest { granted, succeedToShowDialog ->
if (granted) {
startCamera()
// パーミッションが付与されたら onResume でカメラが開始される
} else if (!succeedToShowDialog) {
PermissionDialog.show(this, CAMERA_PERMISSION_REQUEST_KEY)
} else {
Expand All @@ -63,12 +67,16 @@ class MainActivity : AppCompatActivity() {
Settings.get()
}
private var resultSet: Set<ScanResult> = 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 ->
Expand All @@ -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(
Expand All @@ -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()
Expand Down Expand Up @@ -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<Barcode>,
) {
val detected = mutableListOf<Barcode>()
codes.forEach {
codes.forEach { // it を使用
val value = it.rawValue ?: return@forEach
val result = ScanResult(
value = value,
Expand All @@ -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)
}

Expand Down
51 changes: 44 additions & 7 deletions app/src/main/kotlin/net/mm2d/codereader/code/CodeScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,13 +41,20 @@ class CodeScanner(
private val analyzer: CodeAnalyzer = CodeAnalyzer(scanner, callback)
private var camera: Camera? = null
private val torchStateFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)

private var cameraProviderInternal: ProcessCameraProvider? = null
private var previewUseCaseInternal: Preview? = null
private var analysisUseCaseInternal: ImageAnalysis? = null

fun getTouchStateStream(): Flow<Boolean> = torchStateFlow

init {
activity.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
if (event == Event.ON_DESTROY) {
workerExecutor.shutdown()
if (!workerExecutor.isShutdown) {
workerExecutor.shutdown()
}
scanner.close()
}
},
Expand All @@ -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))
}

Expand All @@ -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<UseCase>()
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)
}

Expand Down