Skip to content

Commit 36dfb1a

Browse files
committed
✨ Progress on QR Code Scanner
1 parent e7d87d7 commit 36dfb1a

File tree

13 files changed

+505
-103
lines changed

13 files changed

+505
-103
lines changed
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package co.stonephone.stonecamera
22

33
import android.app.Application
4+
import android.content.Context
45

56
class MyApplication : Application() {
6-
override fun onCreate() {
7-
super.onCreate()
8-
appContext = this
7+
companion object {
8+
private lateinit var instance: MyApplication
9+
10+
fun getAppContext(): Context {
11+
return instance.applicationContext
12+
}
913
}
1014

11-
companion object {
12-
lateinit var appContext: MyApplication
15+
override fun onCreate() {
16+
super.onCreate()
17+
instance = this
1318
}
14-
}
19+
}

app/src/main/java/co/stonephone/stonecamera/StoneCameraApp.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,32 @@ import androidx.compose.ui.unit.dp
2626
import androidx.lifecycle.compose.LocalLifecycleOwner
2727
import androidx.lifecycle.viewmodel.compose.viewModel
2828
import co.stonephone.stonecamera.plugins.AspectRatioPlugin
29+
import co.stonephone.stonecamera.plugins.DebugPlugin
2930
import co.stonephone.stonecamera.plugins.FlashPlugin
3031
import co.stonephone.stonecamera.plugins.FocusBasePlugin
3132
import co.stonephone.stonecamera.plugins.PinchToZoomPlugin
32-
import co.stonephone.stonecamera.plugins.PluginLocation
33+
import co.stonephone.stonecamera.plugins.QRScannerPlugin
3334
import co.stonephone.stonecamera.plugins.SettingLocation
3435
import co.stonephone.stonecamera.plugins.TapToFocusPlugin
3536
import co.stonephone.stonecamera.plugins.ZoomBarPlugin
3637
import co.stonephone.stonecamera.plugins.ZoomBasePlugin
3738
import co.stonephone.stonecamera.ui.RenderPluginSetting
3839
import co.stonephone.stonecamera.ui.ShutterFlashOverlay
3940
import co.stonephone.stonecamera.ui.StoneCameraPreview
40-
import co.stonephone.stonecamera.ui.ZoomBar
4141
import co.stonephone.stonecamera.utils.getAllCamerasInfo
4242

4343
val shootModes = arrayOf("Photo", "Video")
4444

4545
val PLUGINS = listOf(
46+
QRScannerPlugin(),
4647
ZoomBasePlugin(),
4748
ZoomBarPlugin(),
4849
FocusBasePlugin(),
4950
TapToFocusPlugin(),
5051
PinchToZoomPlugin(),
5152
FlashPlugin(),
5253
AspectRatioPlugin(),
54+
// DebugPlugin()
5355
)
5456

5557
@OptIn(ExperimentalCamera2Interop::class)
@@ -71,17 +73,12 @@ fun StoneCameraApp(
7173

7274
val plugins by remember { stoneCameraViewModel::plugins }
7375

74-
val viewfinderPlugins =
75-
plugins.filter { it.pluginLocation == PluginLocation.VIEWFINDER }
76-
val trayPlugins =
77-
plugins.filter { it.pluginLocation == PluginLocation.TRAY }
78-
7976
LaunchedEffect(cameraProvider, lifecycleOwner) {
8077
stoneCameraViewModel.onCameraProvider(cameraProvider)
8178
stoneCameraViewModel.onLifecycleOwner(lifecycleOwner)
8279
}
8380

84-
// We can load cameras once (or whenever context changes) and pass them to the ViewModel
81+
// We can load cameras once (or whenever context changes) and pass them to the ViewModel
8582
LaunchedEffect(Unit) {
8683
val allCameras = getAllCamerasInfo(context)
8784
stoneCameraViewModel.loadCameras(allCameras)
@@ -110,10 +107,11 @@ fun StoneCameraApp(
110107
}
111108
}
112109

113-
viewfinderPlugins.map {
114-
it.render(stoneCameraViewModel, it)
110+
plugins.map {
111+
it.renderViewfinder(stoneCameraViewModel, it)
115112
}
116113

114+
117115
// If showShutterFlash is true, display the overlay
118116
if (showShutterFlash) {
119117
ShutterFlashOverlay(onFlashComplete = {
@@ -128,8 +126,8 @@ fun StoneCameraApp(
128126
.align(Alignment.BottomCenter),
129127
verticalArrangement = Arrangement.SpaceBetween
130128
) {
131-
trayPlugins.map {
132-
it.render(stoneCameraViewModel, it)
129+
plugins.map {
130+
it.renderTray(stoneCameraViewModel, it)
133131
}
134132

135133
// Translucent overlay for mode switch & shutter

app/src/main/java/co/stonephone/stonecamera/StoneCameraAppHelpers.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import androidx.camera.core.ImageCapture
99
import androidx.camera.core.ImageCaptureException
1010
import androidx.camera.video.*
1111
import androidx.core.content.ContextCompat
12-
import co.stonephone.stonecamera.MyApplication.Companion.appContext
1312

1413
object StoneCameraAppHelpers {
1514

@@ -30,14 +29,14 @@ object StoneCameraAppHelpers {
3029
put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/StoneCameraApp")
3130
}
3231
val outputOptions = ImageCapture.OutputFileOptions.Builder(
33-
appContext.contentResolver,
32+
MyApplication.getAppContext().contentResolver,
3433
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
3534
contentValues
3635
).build()
3736

3837
imageCapture.takePicture(
3938
outputOptions,
40-
ContextCompat.getMainExecutor(appContext),
39+
ContextCompat.getMainExecutor(MyApplication.getAppContext()),
4140
object : ImageCapture.OnImageSavedCallback {
4241
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
4342
val savedUri = outputFileResults.savedUri
@@ -65,15 +64,15 @@ object StoneCameraAppHelpers {
6564
}
6665

6766
val outputOptions = MediaStoreOutputOptions.Builder(
68-
appContext.contentResolver,
67+
MyApplication.getAppContext().contentResolver,
6968
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
7069
).setContentValues(contentValues)
7170
.build()
7271

7372
return videoCapture.output
74-
.prepareRecording(appContext, outputOptions)
73+
.prepareRecording(MyApplication.getAppContext(), outputOptions)
7574
.withAudioEnabled()
76-
.start(ContextCompat.getMainExecutor(appContext)) { recordEvent ->
75+
.start(ContextCompat.getMainExecutor(MyApplication.getAppContext())) { recordEvent ->
7776
when (recordEvent) {
7877
is VideoRecordEvent.Start -> {
7978
Log.d(TAG, "Video recording started")

app/src/main/java/co/stonephone/stonecamera/StoneCameraViewModel.kt

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import android.annotation.SuppressLint
55
import android.content.Context
66
import android.content.SharedPreferences
77
import android.net.Uri
8+
import android.util.Log
89
import android.view.MotionEvent
10+
import androidx.annotation.OptIn
911
import androidx.camera.core.*
1012
import androidx.camera.lifecycle.ProcessCameraProvider
1113
import androidx.camera.video.Quality
@@ -24,16 +26,26 @@ import co.stonephone.stonecamera.plugins.PluginSetting
2426
import co.stonephone.stonecamera.utils.StoneCameraInfo
2527
import co.stonephone.stonecamera.utils.createCameraSelectorForId
2628
import co.stonephone.stonecamera.utils.selectCameraForStepZoomLevel
29+
import kotlinx.coroutines.CompletableDeferred
30+
import kotlinx.coroutines.CoroutineScope
31+
import kotlinx.coroutines.Dispatchers
32+
import kotlinx.coroutines.awaitAll
33+
import kotlinx.coroutines.launch
34+
import java.util.concurrent.CountDownLatch
35+
import java.util.concurrent.Executors
2736

2837
@Suppress("UNCHECKED_CAST")
2938
class StoneCameraViewModel(
30-
val context: Context,
39+
context: Context,
3140
private val registeredPlugins: List<IPlugin>
3241
) :
3342
ViewModel() {
3443
private val prefs: SharedPreferences =
3544
context.getSharedPreferences("stone_camera_prefs", Context.MODE_PRIVATE)
3645

46+
private val cameraExecutor = Executors.newSingleThreadExecutor()
47+
48+
3749
//--------------------------------------------------------------------------------
3850
// Mutable state that drives the UI
3951
//--------------------------------------------------------------------------------
@@ -53,9 +65,6 @@ class StoneCameraViewModel(
5365
var facing by mutableStateOf(CameraSelector.LENS_FACING_BACK)
5466
private set
5567

56-
var relativeZoomFactor by mutableStateOf(1f)
57-
private set
58-
5968
var isRecording by mutableStateOf(false)
6069
private set
6170

@@ -102,6 +111,8 @@ class StoneCameraViewModel(
102111

103112
var imageCapture: ImageCapture = createImageCapture()
104113

114+
var imageAnalysis: ImageAnalysis = createImageAnalysis()
115+
105116
val recorder: Recorder = Recorder.Builder()
106117
.setQualitySelector(QualitySelector.from(Quality.HD))
107118
.build()
@@ -142,11 +153,7 @@ class StoneCameraViewModel(
142153
}
143154

144155
fun onPreviewView(view: PreviewView) {
145-
val _view = plugins.fold(view) { v, plugin ->
146-
plugin.onPreviewView(this, v)
147-
}
148-
149-
_previewView = _view
156+
_previewView = view
150157
bindUseCases()
151158
}
152159

@@ -339,16 +346,20 @@ class StoneCameraViewModel(
339346
previewViewTouchHandlers.clear()
340347
_cameraProvider!!.unbindAll()
341348

342-
343349
camera = _cameraProvider!!.bindToLifecycle(
344350
lifecycleOwner!!,
345351
cameraSelector,
346352
preview,
347353
imageCapture,
348-
videoCapture
354+
videoCapture,
355+
imageAnalysis,
349356
)
350357

351358
initializePlugins()
359+
360+
_previewView = plugins.fold(previewView!!) { v, plugin ->
361+
plugin.onPreviewView(this, v)
362+
}
352363
} catch (e: Exception) {
353364
// Handle binding errors
354365
e.printStackTrace()
@@ -359,6 +370,7 @@ class StoneCameraViewModel(
359370
fun recreateUseCases() {
360371
preview = createPreview()
361372
imageCapture = createImageCapture()
373+
imageAnalysis = createImageAnalysis()
362374
// TODO: videoCapture
363375

364376
this.bindUseCases()
@@ -376,6 +388,43 @@ class StoneCameraViewModel(
376388
}.build()
377389
}
378390

391+
@OptIn(ExperimentalGetImage::class)
392+
fun createImageAnalysis(): ImageAnalysis {
393+
val analysis = ImageAnalysis.Builder()
394+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
395+
.build()
396+
.also {
397+
it.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer { imageProxy ->
398+
val inputImage = imageProxy.image ?: return@Analyzer
399+
400+
// Create a list to store the work objects
401+
val workList = mutableListOf<CompletableDeferred<Unit>>()
402+
403+
val analysisPlugins = plugins.filter { it.onImageAnalysis != null }
404+
// Dispatch work to each plugin
405+
for (plugin in analysisPlugins) {
406+
val work = plugin.onImageAnalysis!!(this, imageProxy, inputImage)
407+
workList.add(work)
408+
}
409+
410+
// Use coroutines to run the plugins in parallel and wait for all to complete
411+
CoroutineScope(Dispatchers.IO).launch {
412+
try {
413+
// Await all work to finish
414+
workList.awaitAll()
415+
Log.d("Analyzer", "All plugins completed successfully")
416+
} catch (e: Exception) {
417+
Log.e("Analyzer", "Error in one or more plugins", e)
418+
} finally {
419+
// Close the ImageProxy after all plugins are done
420+
imageProxy.close()
421+
}
422+
}
423+
})
424+
}
425+
426+
return analysis;
427+
}
379428
}
380429

381430
class StoneCameraViewModelFactory(

app/src/main/java/co/stonephone/stonecamera/plugins/AspectRatio.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package co.stonephone.stonecamera.plugins
22

3+
import android.app.Application
34
import android.util.Size
45
import androidx.camera.core.AspectRatio
56
import androidx.camera.core.ImageCapture
@@ -13,8 +14,10 @@ import androidx.compose.material3.MaterialTheme
1314
import androidx.compose.material3.Text
1415
import androidx.compose.ui.Modifier
1516
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.platform.LocalContext
1618
import androidx.compose.ui.text.font.FontWeight
1719
import androidx.compose.ui.unit.dp
20+
import co.stonephone.stonecamera.MyApplication
1821
import co.stonephone.stonecamera.StoneCameraViewModel
1922
import co.stonephone.stonecamera.utils.getLargestMatchingSize
2023
import co.stonephone.stonecamera.utils.getLargestSensorSize
@@ -24,7 +27,13 @@ class AspectRatioPlugin : IPlugin {
2427
override val name: String = "Aspect Ratio"
2528

2629
override fun initialize(viewModel: StoneCameraViewModel) {
27-
30+
val previewView = viewModel.previewView
31+
val aspectRatio = viewModel.getSetting<String>("aspectRatio")
32+
if (aspectRatio === "FULL") {
33+
previewView!!.scaleType = PreviewView.ScaleType.FILL_CENTER
34+
} else {
35+
previewView!!.scaleType = PreviewView.ScaleType.FIT_CENTER
36+
}
2837
}
2938

3039
override fun onImageCapture(
@@ -33,7 +42,7 @@ class AspectRatioPlugin : IPlugin {
3342
): ImageCapture.Builder {
3443
// TODO: "FULL" aspect ratio isn't cropping on image capture
3544
val ratioStr = viewModel.getSetting<String>("aspectRatio") ?: "16:9"
36-
val context = viewModel.context
45+
val context = MyApplication.getAppContext()
3746

3847
val ratioOrNull = parseRatioOrNull(ratioStr)
3948

@@ -55,7 +64,7 @@ class AspectRatioPlugin : IPlugin {
5564
): Preview.Builder {
5665
val ratioStr = viewModel.getSetting<String>("aspectRatio") ?: "16:9"
5766

58-
val context = viewModel.context
67+
val context = MyApplication.getAppContext()
5968
val ratioOrNull = parseRatioOrNull(ratioStr)
6069

6170
// 1) Figure out the best "preferred size" for this ratio
@@ -144,7 +153,8 @@ class AspectRatioPlugin : IPlugin {
144153
viewModel: StoneCameraViewModel,
145154
previewView: PreviewView
146155
): PreviewView {
147-
if (viewModel.getSetting<String>("aspectRatio") === "FULL") {
156+
val aspectRatio = viewModel.getSetting<String>("aspectRatio")
157+
if (aspectRatio == "FULL") {
148158
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
149159
} else {
150160
previewView.scaleType = PreviewView.ScaleType.FIT_CENTER

0 commit comments

Comments
 (0)