feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,44 @@
apply plugin: 'com.android.library'
group = 'host.exp.exponent'
version = '15.0.16'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useDefaultAndroidSdkVersions()
useExpoPublishing()
android {
namespace "expo.modules.camera"
defaultConfig {
versionCode 32
versionName "15.0.16"
}
}
repositories {
maven {
url "$projectDir/maven"
}
}
dependencies {
def camerax_version = "1.4.0-beta02"
api "androidx.exifinterface:exifinterface:1.3.7"
api "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
implementation "com.google.mlkit:barcode-scanning:17.2.0"
implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
api 'com.google.android:cameraview:1.0.0'
}

View File

@@ -0,0 +1 @@
4c23883e5472108b7a31944c4ba58eea

View File

@@ -0,0 +1 @@
3874291cb50122dd79eba6d49524e0be58bb8f6f

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.android</groupId>
<artifactId>cameraview</artifactId>
<version>1.0.0</version>
<packaging>aar</packaging>
<dependencies>
<dependency>
<groupId>com.android.support</groupId>
<artifactId>support-annotations</artifactId>
<version>25.3.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.android.support</groupId>
<artifactId>support-v4</artifactId>
<version>25.3.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.android.support</groupId>
<artifactId>appcompat-v7</artifactId>
<version>25.3.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1 @@
e8661f352c640fe9d420e153a0aa095d

View File

@@ -0,0 +1 @@
cd83ff235770e2d944b18f70f881c29b4f935599

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>com.google.android</groupId>
<artifactId>cameraview</artifactId>
<versioning>
<release>1.0.0</release>
<versions>
<version>1.0.0</version>
</versions>
<lastUpdated>20180605124508</lastUpdated>
</versioning>
</metadata>

View File

@@ -0,0 +1 @@
bea89333cd34959f72e685c639ed8e42

View File

@@ -0,0 +1 @@
b9227e1e2d046cd0fb25f555bc5d16ee240c27f5

View File

@@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>

View File

@@ -0,0 +1,15 @@
package expo.modules.camera
import expo.modules.kotlin.exception.CodedException
class CameraExceptions {
class ImageCaptureFailed : CodedException(message = "Failed to capture image")
class VideoRecordingFailed(cause: String?) : CodedException("Video recording failed: $cause")
class ImageRetrievalException(url: String) :
CodedException("Could not get the image from given url: '$url'")
class UnsupportedAspectRatioException(aspectRatio: String) :
CodedException("Unsupported aspect ratio: '$aspectRatio'")
}

View File

@@ -0,0 +1,42 @@
package expo.modules.camera
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import expo.modules.camera.records.CameraType
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
object CameraViewHelper {
// Utilities
@JvmStatic
fun getCorrectCameraRotation(rotation: Int, facing: CameraType) =
if (facing == CameraType.FRONT) {
(rotation - 90 + 360) % 360
} else {
(-rotation + 90 + 360) % 360
}
fun generateSimulatorPhoto(width: Int, height: Int): ByteArray {
val fakePhotoBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(fakePhotoBitmap)
val background = Paint().apply {
color = Color.BLACK
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), background)
val textPaint = Paint().apply {
color = Color.YELLOW
textSize = 35f
}
val calendar = Calendar.getInstance()
val simpleDateFormat = SimpleDateFormat("dd.MM.yy HH:mm:ss", Locale.US)
canvas.drawText(simpleDateFormat.format(calendar.time), width * 0.1f, height * 0.9f, textPaint)
val stream = ByteArrayOutputStream()
fakePhotoBitmap.compress(Bitmap.CompressFormat.PNG, 90, stream)
return stream.toByteArray()
}
}

View File

@@ -0,0 +1,263 @@
package expo.modules.camera
import android.Manifest
import android.graphics.Bitmap
import android.util.Log
import expo.modules.camera.analyzers.BarCodeScannerResultSerializer
import expo.modules.camera.analyzers.MLKitBarCodeScanner
import expo.modules.camera.records.BarcodeSettings
import expo.modules.camera.records.BarcodeType
import expo.modules.camera.records.CameraMode
import expo.modules.camera.records.CameraRatio
import expo.modules.camera.records.CameraType
import expo.modules.camera.records.FlashMode
import expo.modules.camera.records.FocusMode
import expo.modules.camera.records.VideoQuality
import expo.modules.camera.tasks.ResolveTakenPicture
import expo.modules.core.errors.ModuleDestroyedException
import expo.modules.core.utilities.EmulatorUtilities
import expo.modules.interfaces.imageloader.ImageLoaderInterface
import expo.modules.interfaces.permissions.Permissions
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Queues
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
val cameraEvents = arrayOf(
"onCameraReady",
"onMountError",
"onBarcodeScanned",
"onFacesDetected",
"onFaceDetectionError",
"onPictureSaved"
)
class CameraViewModule : Module() {
private val moduleScope = CoroutineScope(Dispatchers.Main)
override fun definition() = ModuleDefinition {
Name("ExpoCamera")
Events("onModernBarcodeScanned")
AsyncFunction("requestCameraPermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.CAMERA
)
}
AsyncFunction("requestMicrophonePermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.RECORD_AUDIO
)
}
AsyncFunction("getCameraPermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.CAMERA
)
}
AsyncFunction("getMicrophonePermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.RECORD_AUDIO
)
}
AsyncFunction("scanFromURLAsync") { url: String, barcodeTypes: List<BarcodeType>, promise: Promise ->
appContext.imageLoader?.loadImageForManipulationFromURL(
url,
object : ImageLoaderInterface.ResultListener {
override fun onSuccess(bitmap: Bitmap) {
val scanner = MLKitBarCodeScanner()
val formats = barcodeTypes.map { it.mapToBarcode() }
scanner.setSettings(formats)
moduleScope.launch {
val barcodes = scanner.scan(bitmap)
.filter { formats.contains(it.type) }
.map { BarCodeScannerResultSerializer.toBundle(it, 1.0f) }
promise.resolve(barcodes)
}
}
override fun onFailure(cause: Throwable?) {
promise.reject(CameraExceptions.ImageRetrievalException(url))
}
}
)
}
OnDestroy {
try {
moduleScope.cancel(ModuleDestroyedException())
} catch (e: IllegalStateException) {
Log.e(TAG, "The scope does not have a job in it")
}
}
View(ExpoCameraView::class) {
Events(cameraEvents)
Prop("facing") { view, facing: CameraType? ->
facing?.let {
if (view.lensFacing != facing) {
view.lensFacing = it
}
}
}
Prop("flashMode") { view, flashMode: FlashMode? ->
flashMode?.let {
view.setCameraFlashMode(it)
}
}
Prop("enableTorch") { view, enabled: Boolean? ->
view.enableTorch = enabled ?: false
}
Prop("animateShutter") { view, animate: Boolean? ->
view.animateShutter = animate ?: true
}
Prop("zoom") { view, zoom: Float? ->
zoom?.let {
view.camera?.cameraControl?.setLinearZoom(it)
}
}
Prop("mode") { view, mode: CameraMode? ->
mode?.let {
if (view.cameraMode != mode) {
view.cameraMode = it
}
}
}
Prop("mute") { view, muted: Boolean? ->
muted?.let {
if (it != view.mute) {
view.mute = it
}
}
}
Prop("videoQuality") { view, quality: VideoQuality? ->
quality?.let {
view.videoQuality = it
}
}
Prop("barcodeScannerSettings") { view, settings: BarcodeSettings? ->
if (settings == null) {
return@Prop
}
view.setBarcodeScannerSettings(settings)
}
Prop("barcodeScannerEnabled") { view, enabled: Boolean? ->
enabled?.let {
view.setShouldScanBarcodes(enabled)
}
}
Prop("pictureSize") { view, pictureSize: String? ->
pictureSize?.let {
if (view.pictureSize != pictureSize) {
view.pictureSize = it
}
}
}
Prop("autoFocus") { view, autoFocus: FocusMode? ->
view.autoFocus = autoFocus ?: FocusMode.OFF
}
Prop("ratio") { view, ratio: CameraRatio? ->
if (view.ratio != ratio) {
view.ratio = ratio
}
}
Prop("mirror") { view, mirror: Boolean? ->
mirror?.let {
view.mirror = it
return@Prop
}
view.mirror = false
}
OnViewDidUpdateProps { view ->
view.createCamera()
}
AsyncFunction("takePicture") { view: ExpoCameraView, options: PictureOptions, promise: Promise ->
if (!EmulatorUtilities.isRunningOnEmulator()) {
view.takePicture(options, promise, cacheDirectory)
} else {
val image = CameraViewHelper.generateSimulatorPhoto(view.width, view.height)
moduleScope.launch {
ResolveTakenPicture(image, promise, options, false, cacheDirectory) { response ->
view.onPictureSaved(response)
}.resolve()
}
}
}.runOnQueue(Queues.MAIN)
AsyncFunction("getAvailablePictureSizes") { view: ExpoCameraView ->
return@AsyncFunction view.getAvailablePictureSizes()
}
AsyncFunction("record") { view: ExpoCameraView, options: RecordingOptions, promise: Promise ->
if (!view.mute && !permissionsManager.hasGrantedPermissions(Manifest.permission.RECORD_AUDIO)) {
throw Exceptions.MissingPermissions(Manifest.permission.RECORD_AUDIO)
}
view.record(options, promise, cacheDirectory)
}.runOnQueue(Queues.MAIN)
AsyncFunction("stopRecording") { view: ExpoCameraView ->
view.activeRecording?.close()
}.runOnQueue(Queues.MAIN)
AsyncFunction("resumePreview") { view: ExpoCameraView ->
view.resumePreview()
}
AsyncFunction("pausePreview") { view: ExpoCameraView ->
view.pausePreview()
}
OnViewDestroys { view ->
view.orientationEventListener.disable()
view.cancelCoroutineScope()
view.releaseCamera()
}
}
}
private val cacheDirectory: File
get() = appContext.cacheDirectory
private val permissionsManager: Permissions
get() = appContext.permissions ?: throw Exceptions.PermissionsModuleNotFound()
companion object {
internal val TAG = CameraViewModule::class.java.simpleName
}
}

View File

@@ -0,0 +1,655 @@
package expo.modules.camera
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.ImageFormat
import android.graphics.SurfaceTexture
import android.graphics.drawable.ColorDrawable
import android.hardware.camera2.CameraCharacteristics
import android.media.AudioManager
import android.media.MediaActionSound
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.Size
import android.view.OrientationEventListener
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraState
import androidx.camera.core.DisplayOrientedMeteringPointFactory
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.MirrorMode
import androidx.camera.core.Preview
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.FileOutputOptions
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import expo.modules.camera.analyzers.BarcodeAnalyzer
import expo.modules.camera.analyzers.toByteArray
import expo.modules.camera.common.BarcodeScannedEvent
import expo.modules.camera.common.CameraMountErrorEvent
import expo.modules.camera.common.PictureSavedEvent
import expo.modules.camera.records.BarcodeSettings
import expo.modules.camera.records.BarcodeType
import expo.modules.camera.records.CameraMode
import expo.modules.camera.records.CameraRatio
import expo.modules.camera.records.CameraType
import expo.modules.camera.records.FlashMode
import expo.modules.camera.records.FocusMode
import expo.modules.camera.records.VideoQuality
import expo.modules.camera.tasks.ResolveTakenPicture
import expo.modules.camera.utils.FileSystemUtils
import expo.modules.camera.utils.mapX
import expo.modules.camera.utils.mapY
import expo.modules.core.errors.ModuleDestroyedException
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult.BoundingBox
import expo.modules.interfaces.camera.CameraViewInterface
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.File
import kotlin.math.roundToInt
import kotlin.properties.Delegates
const val ANIMATION_FAST_MILLIS = 50L
const val ANIMATION_SLOW_MILLIS = 100L
@SuppressLint("ViewConstructor")
class ExpoCameraView(
context: Context,
appContext: AppContext
) : ExpoView(context, appContext),
CameraViewInterface {
private val currentActivity
get() = appContext.currentActivity as? AppCompatActivity
?: throw Exceptions.MissingActivity()
val orientationEventListener by lazy {
object : OrientationEventListener(currentActivity) {
override fun onOrientationChanged(orientation: Int) {
if (orientation == ORIENTATION_UNKNOWN) {
return
}
val rotation = when (orientation) {
in 45 until 135 -> Surface.ROTATION_270
in 135 until 225 -> Surface.ROTATION_180
in 225 until 315 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
imageAnalysisUseCase?.targetRotation = rotation
imageCaptureUseCase?.targetRotation = rotation
}
}
}
var camera: Camera? = null
var activeRecording: Recording? = null
private var cameraProvider: ProcessCameraProvider? = null
private val providerFuture = ProcessCameraProvider.getInstance(context)
private var imageCaptureUseCase: ImageCapture? = null
private var imageAnalysisUseCase: ImageAnalysis? = null
private var recorder: Recorder? = null
private var barcodeFormats: List<BarcodeType> = emptyList()
private var previewView = PreviewView(context).apply {
elevation = 0f
}
private val scope = CoroutineScope(Dispatchers.Main)
private var shouldCreateCamera = false
private var previewPaused = false
var lensFacing = CameraType.BACK
set(value) {
field = value
shouldCreateCamera = true
}
var cameraMode: CameraMode = CameraMode.PICTURE
set(value) {
field = value
shouldCreateCamera = true
}
var autoFocus: FocusMode = FocusMode.OFF
set(value) {
field = value
camera?.cameraControl?.let {
if (field == FocusMode.OFF) {
it.cancelFocusAndMetering()
} else {
startFocusMetering()
}
}
}
var videoQuality: VideoQuality = VideoQuality.VIDEO1080P
set(value) {
field = value
shouldCreateCamera = true
}
var ratio: CameraRatio? = null
set(value) {
field = value
shouldCreateCamera = true
}
var pictureSize: String = ""
set(value) {
field = value
shouldCreateCamera = true
}
var mirror: Boolean = false
set(value) {
field = value
shouldCreateCamera = true
}
var mute: Boolean = false
var animateShutter: Boolean = true
var enableTorch: Boolean by Delegates.observable(false) { _, _, newValue ->
setTorchEnabled(newValue)
}
private val onCameraReady by EventDispatcher<Unit>()
private val onMountError by EventDispatcher<CameraMountErrorEvent>()
private val onBarcodeScanned by EventDispatcher<BarcodeScannedEvent>(
/**
* We want every distinct barcode to be reported to the JS listener.
* If we return some static value as a coalescing key there may be two barcode events
* containing two different barcodes waiting to be transmitted to JS
* that would get coalesced (because both of them would have the same coalescing key).
* So let's differentiate them with a hash of the contents (mod short's max value).
*/
coalescingKey = { event -> (event.data.hashCode() % Short.MAX_VALUE).toShort() }
)
private val onPictureSaved by EventDispatcher<PictureSavedEvent>(
coalescingKey = { event ->
val uriHash = event.data.getString("uri")?.hashCode() ?: -1
(uriHash % Short.MAX_VALUE).toShort()
}
)
// Scanning-related properties
private var shouldScanBarcodes = false
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
measureChild(previewView, widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(
ViewGroup.resolveSize(previewView.measuredWidth, widthMeasureSpec),
ViewGroup.resolveSize(previewView.measuredHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
if (!changed) {
return
}
val width = right - left
val height = bottom - top
previewView.layout(0, 0, width, height)
}
override fun onViewAdded(child: View?) {
super.onViewAdded(child)
if (child == previewView) {
return
}
child?.bringToFront()
removeView(previewView)
addView(previewView, 0)
}
fun takePicture(options: PictureOptions, promise: Promise, cacheDirectory: File) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
imageCaptureUseCase?.takePicture(
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureStarted() {
if (volume != 0) {
MediaActionSound().play(MediaActionSound.SHUTTER_CLICK)
}
if (!animateShutter) {
return
}
rootView.postDelayed({
rootView.foreground = ColorDrawable(Color.WHITE)
rootView.postDelayed(
{ rootView.foreground = null },
ANIMATION_FAST_MILLIS
)
}, ANIMATION_SLOW_MILLIS)
}
override fun onCaptureSuccess(image: ImageProxy) {
val data = image.planes.toByteArray()
if (options.fastMode) {
promise.resolve(null)
}
cacheDirectory.let {
scope.launch {
val shouldMirror = mirror && lensFacing == CameraType.FRONT
ResolveTakenPicture(data, promise, options, shouldMirror, it) { response: Bundle ->
onPictureSaved(response)
}.resolve()
}
}
image.close()
}
override fun onError(exception: ImageCaptureException) {
promise.reject(CameraExceptions.ImageCaptureFailed())
}
}
)
}
fun setCameraFlashMode(mode: FlashMode) {
if (imageCaptureUseCase?.flashMode != mode.mapToLens()) {
imageCaptureUseCase?.flashMode = mode.mapToLens()
}
}
private fun setTorchEnabled(enabled: Boolean) {
if (camera?.cameraInfo?.hasFlashUnit() == true) {
camera?.cameraControl?.enableTorch(enabled)
}
}
fun record(options: RecordingOptions, promise: Promise, cacheDirectory: File) {
val file = FileSystemUtils.generateOutputFile(cacheDirectory, "Camera", ".mp4")
val fileOutputOptions = FileOutputOptions.Builder(file)
.setFileSizeLimit(options.maxFileSize.toLong())
.setDurationLimitMillis(options.maxDuration.toLong() * 1000)
.build()
recorder?.let {
if (!mute && ActivityCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
promise.reject(Exceptions.MissingPermissions(Manifest.permission.RECORD_AUDIO))
return
}
activeRecording = it.prepareRecording(context, fileOutputOptions)
.apply {
if (!mute) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { event ->
when (event) {
is VideoRecordEvent.Finalize -> {
when (event.error) {
VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED,
VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED,
VideoRecordEvent.Finalize.ERROR_NONE -> {
promise.resolve(
Bundle().apply {
putString("uri", event.outputResults.outputUri.toString())
}
)
}
else -> promise.reject(
CameraExceptions.VideoRecordingFailed(
event.cause?.message
?: "Video recording Failed: ${event.cause?.message ?: "Unknown error"}"
)
)
}
}
}
}
}
?: promise.reject("E_RECORDING_FAILED", "Starting video recording failed - could not create video file.", null)
}
@SuppressLint("UnsafeOptInUsageError")
fun createCamera() {
if (!shouldCreateCamera || previewPaused) {
return
}
shouldCreateCamera = false
providerFuture.addListener(
{
val cameraProvider: ProcessCameraProvider = providerFuture.get()
previewView.scaleType = if (ratio == CameraRatio.FOUR_THREE || ratio == CameraRatio.SIXTEEN_NINE) {
PreviewView.ScaleType.FIT_CENTER
} else {
PreviewView.ScaleType.FILL_CENTER
}
val resolutionSelector = buildResolutionSelector()
val preview = Preview.Builder()
.setResolutionSelector(resolutionSelector)
.build()
.also {
it.surfaceProvider = previewView.surfaceProvider
}
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing.mapToCharacteristic())
.build()
imageCaptureUseCase = ImageCapture.Builder()
.setResolutionSelector(resolutionSelector)
.build()
val videoCapture = createVideoCapture()
imageAnalysisUseCase = createImageAnalyzer()
val useCases = UseCaseGroup.Builder().apply {
addUseCase(preview)
if (cameraMode == CameraMode.PICTURE) {
imageCaptureUseCase?.let {
addUseCase(it)
}
imageAnalysisUseCase?.let {
addUseCase(it)
}
} else {
addUseCase(videoCapture)
}
}.build()
try {
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(currentActivity, cameraSelector, useCases)
camera?.let {
observeCameraState(it.cameraInfo)
}
this.cameraProvider = cameraProvider
} catch (e: Exception) {
onMountError(
CameraMountErrorEvent("Camera component could not be rendered - is there any other instance running?")
)
}
},
ContextCompat.getMainExecutor(context)
)
}
private fun createImageAnalyzer(): ImageAnalysis =
ImageAnalysis.Builder()
.setResolutionSelector(
ResolutionSelector.Builder()
.setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
.build()
)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { analyzer ->
if (shouldScanBarcodes) {
analyzer.setAnalyzer(
ContextCompat.getMainExecutor(context),
BarcodeAnalyzer(lensFacing, barcodeFormats) {
onBarcodeScanned(it)
}
)
}
}
private fun buildResolutionSelector(): ResolutionSelector {
val strategy = if (pictureSize.isNotEmpty()) {
val size = Size.parseSize(pictureSize)
ResolutionStrategy(size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER)
} else {
ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
}
return if (ratio == CameraRatio.ONE_ONE) {
ResolutionSelector.Builder().setResolutionFilter { supportedSizes, _ ->
return@setResolutionFilter supportedSizes.filter {
it.width == it.height
}
}.setResolutionStrategy(strategy).build()
} else {
ResolutionSelector.Builder().apply {
ratio?.let {
setAspectRatioStrategy(it.mapToStrategy())
}
setResolutionStrategy(strategy)
}.build()
}
}
private fun createVideoCapture(): VideoCapture<Recorder> {
val preferredQuality = videoQuality.mapToQuality()
val fallbackStrategy = FallbackStrategy.higherQualityOrLowerThan(preferredQuality)
val qualitySelector = QualitySelector.from(preferredQuality, fallbackStrategy)
val recorder = Recorder.Builder()
.setExecutor(ContextCompat.getMainExecutor(context))
.setQualitySelector(qualitySelector)
.build()
.also {
this.recorder = it
}
return VideoCapture.Builder(recorder).apply {
if (mirror) {
setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY)
}
setVideoStabilizationEnabled(true)
}.build()
}
private fun startFocusMetering() {
camera?.let {
val meteringPointFactory = DisplayOrientedMeteringPointFactory(
previewView.display,
it.cameraInfo,
previewView.width.toFloat(),
previewView.height.toFloat()
)
val action = FocusMeteringAction.Builder(meteringPointFactory.createPoint(1f, 1f), FocusMeteringAction.FLAG_AF)
.build()
it.cameraControl.startFocusAndMetering(action)
}
}
private fun observeCameraState(cameraInfo: CameraInfo) {
cameraInfo.cameraState.observe(currentActivity) {
when (it.type) {
CameraState.Type.OPEN -> {
onCameraReady(Unit)
setTorchEnabled(enableTorch)
}
else -> {}
}
}
}
@OptIn(ExperimentalCamera2Interop::class)
fun getAvailablePictureSizes(): List<String> {
return camera?.cameraInfo?.let { cameraInfo ->
val info = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
info?.getOutputSizes(ImageFormat.JPEG)?.map { it.toString() }
} ?: emptyList()
}
fun resumePreview() {
shouldCreateCamera = true
previewPaused = false
createCamera()
}
fun pausePreview() {
previewPaused = true
cameraProvider?.unbindAll()
}
fun setShouldScanBarcodes(shouldScanBarcodes: Boolean) {
this.shouldScanBarcodes = shouldScanBarcodes
shouldCreateCamera = true
}
fun setBarcodeScannerSettings(settings: BarcodeSettings?) {
barcodeFormats = settings?.barcodeTypes ?: emptyList()
}
private fun getDeviceOrientation() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
appContext.currentActivity?.display?.rotation ?: 0
} else {
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation
}
fun releaseCamera() = appContext.mainQueue.launch {
cameraProvider?.unbindAll()
}
private fun transformBarcodeScannerResultToViewCoordinates(barcode: BarCodeScannerResult) {
val cornerPoints = barcode.cornerPoints
val previewWidth = previewView.width
val previewHeight = previewView.height
val facingFront = lensFacing == CameraType.FRONT
val portrait = getDeviceOrientation() % 2 == 0
val landscape = getDeviceOrientation() % 2 != 0
if (facingFront && portrait) {
cornerPoints.mapY { barcode.referenceImageHeight - cornerPoints[it] }
}
if (facingFront && landscape) {
cornerPoints.mapX { barcode.referenceImageWidth - cornerPoints[it] }
}
cornerPoints.mapX {
(cornerPoints[it] * previewWidth / barcode.referenceImageWidth.toFloat())
.roundToInt()
}
cornerPoints.mapY {
(cornerPoints[it] * previewHeight / barcode.referenceImageHeight.toFloat())
.roundToInt()
}
barcode.cornerPoints = cornerPoints
barcode.referenceImageHeight = height
barcode.referenceImageWidth = width
}
private fun getCornerPointsAndBoundingBox(cornerPoints: List<Int>, boundingBox: BoundingBox): Pair<ArrayList<Bundle>, Bundle> {
val density = previewView.resources.displayMetrics.density
val convertedCornerPoints = ArrayList<Bundle>()
for (i in cornerPoints.indices step 2) {
val y = cornerPoints[i].toFloat() / density
val x = cornerPoints[i + 1].toFloat() / density
convertedCornerPoints.add(
Bundle().apply {
putFloat("x", x)
putFloat("y", y)
}
)
}
val boundingBoxBundle = Bundle().apply {
putParcelable(
"origin",
Bundle().apply {
putFloat("x", boundingBox.x.toFloat() / density)
putFloat("y", boundingBox.y.toFloat() / density)
}
)
putParcelable(
"size",
Bundle().apply {
putFloat("width", boundingBox.width.toFloat() / density)
putFloat("height", boundingBox.height.toFloat() / density)
}
)
}
return convertedCornerPoints to boundingBoxBundle
}
private fun onBarcodeScanned(barcode: BarCodeScannerResult) {
if (shouldScanBarcodes) {
transformBarcodeScannerResultToViewCoordinates(barcode)
val (cornerPoints, boundingBox) = getCornerPointsAndBoundingBox(barcode.cornerPoints, barcode.boundingBox)
onBarcodeScanned(
BarcodeScannedEvent(
target = id,
data = barcode.value,
raw = barcode.raw,
type = BarcodeType.mapFormatToString(barcode.type),
cornerPoints = cornerPoints,
boundingBox = boundingBox
)
)
}
}
override fun setPreviewTexture(surfaceTexture: SurfaceTexture?) = Unit
override fun getPreviewSizeAsArray() = intArrayOf(previewView.width, previewView.height)
init {
orientationEventListener.enable()
previewView.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
override fun onChildViewAdded(parent: View?, child: View?) {
parent?.measure(
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
)
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
}
})
addView(
previewView,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
}
fun onPictureSaved(response: Bundle) {
onPictureSaved(PictureSavedEvent(response.getInt("id"), response.getBundle("data")!!))
}
fun cancelCoroutineScope() = try {
scope.cancel(ModuleDestroyedException())
} catch (e: Exception) {
Log.e(CameraViewModule.TAG, "The scope does not have a job in it")
}
}

View File

@@ -0,0 +1,23 @@
package expo.modules.camera
import expo.modules.camera.records.VideoQuality
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
data class PictureOptions(
@Field val quality: Double = 1.0,
@Field val base64: Boolean = false,
@Field val exif: Boolean = false,
@Field val additionalExif: Map<String, Any>? = null,
@Field val mirror: Boolean = false,
@Field val skipProcessing: Boolean = false,
@Field val fastMode: Boolean = false,
@Field val id: Int? = null,
@Field val maxDownsampling: Int = 1
) : Record
data class RecordingOptions(
@Field val maxDuration: Int = 0,
@Field val maxFileSize: Int = 0,
@Field val quality: VideoQuality?
) : Record

View File

@@ -0,0 +1,76 @@
package expo.modules.camera.analyzers
import android.util.Log
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import expo.modules.camera.CameraViewHelper
import expo.modules.camera.records.BarcodeType
import expo.modules.camera.records.CameraType
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
import java.nio.ByteBuffer
@OptIn(ExperimentalGetImage::class)
class BarcodeAnalyzer(private val lensFacing: CameraType, formats: List<BarcodeType>, val onComplete: (BarCodeScannerResult) -> Unit) : ImageAnalysis.Analyzer {
private val barcodeFormats = if (formats.isEmpty()) {
0
} else {
formats.map { it.mapToBarcode() }.reduce { acc, it ->
acc or it
}
}
private var barcodeScannerOptions =
BarcodeScannerOptions.Builder()
.setBarcodeFormats(barcodeFormats)
.build()
private var barcodeScanner = BarcodeScanning.getClient(barcodeScannerOptions)
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
val rotation = CameraViewHelper.getCorrectCameraRotation(imageProxy.imageInfo.rotationDegrees, lensFacing)
val image = InputImage.fromMediaImage(mediaImage, rotation)
barcodeScanner.process(image)
.addOnSuccessListener { barcodes ->
if (barcodes.isEmpty()) {
return@addOnSuccessListener
}
val barcode = barcodes.first()
val raw = barcode.rawValue ?: barcode.rawBytes?.let { String(it) }
val cornerPoints = mutableListOf<Int>()
barcode.cornerPoints?.let { points ->
for (point in points) {
cornerPoints.addAll(listOf(point.x, point.y))
}
}
onComplete(BarCodeScannerResult(barcode.format, barcode.displayValue, raw, cornerPoints, image.width, image.height))
}
.addOnFailureListener {
Log.d("SCANNER", it.cause?.message ?: "Barcode scanning failed")
}
.addOnCompleteListener {
imageProxy.close()
}
}
}
}
private fun ByteBuffer.toByteArray(): ByteArray {
rewind()
val data = ByteArray(remaining())
get(data)
return data
}
fun Array<ImageProxy.PlaneProxy>.toByteArray() = this.fold(mutableListOf<Byte>()) { acc, plane ->
acc.addAll(plane.buffer.toByteArray().toList())
acc
}.toByteArray()

View File

@@ -0,0 +1,48 @@
package expo.modules.camera.analyzers
import android.os.Bundle
import android.util.Pair
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
object BarCodeScannerResultSerializer {
fun toBundle(result: BarCodeScannerResult, density: Float) =
Bundle().apply {
putString("data", result.value)
putString("raw", result.raw)
putInt("type", result.type)
val cornerPointsAndBoundingBox = getCornerPointsAndBoundingBox(result.cornerPoints, result.boundingBox, density)
putParcelableArrayList("cornerPoints", cornerPointsAndBoundingBox.first)
putBundle("bounds", cornerPointsAndBoundingBox.second)
}
private fun getCornerPointsAndBoundingBox(
cornerPoints: List<Int>,
boundingBox: BarCodeScannerResult.BoundingBox,
density: Float
): Pair<ArrayList<Bundle>, Bundle> {
val convertedCornerPoints = ArrayList<Bundle>()
for (i in cornerPoints.indices step 2) {
val x = cornerPoints[i].toFloat() / density
val y = cornerPoints[i + 1].toFloat() / density
convertedCornerPoints.add(getPoint(x, y))
}
val boundingBoxBundle = Bundle().apply {
putParcelable("origin", getPoint(boundingBox.x.toFloat() / density, boundingBox.y.toFloat() / density))
putParcelable("size", getSize(boundingBox.width.toFloat() / density, boundingBox.height.toFloat() / density))
}
return Pair(convertedCornerPoints, boundingBoxBundle)
}
private fun getSize(width: Float, height: Float) =
Bundle().apply {
putFloat("width", width)
putFloat("height", height)
}
private fun getPoint(x: Float, y: Float) =
Bundle().apply {
putFloat("x", x)
putFloat("y", y)
}
}

View File

@@ -0,0 +1,102 @@
package expo.modules.camera.analyzers
import android.graphics.Bitmap
import android.util.Log
import com.google.android.gms.tasks.Task
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class MLKitBarCodeScanner {
private var barCodeTypes: List<Int>? = null
private var barcodeScannerOptions =
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.build()
private var barcodeScanner = BarcodeScanning.getClient(barcodeScannerOptions)
suspend fun scan(bitmap: Bitmap): List<BarCodeScannerResult> = withContext(Dispatchers.IO) {
val inputImage = InputImage.fromBitmap(bitmap, 0)
try {
val result: List<Barcode> = barcodeScanner.process(inputImage).await()
val results = mutableListOf<BarCodeScannerResult>()
if (result.isEmpty()) {
return@withContext results
}
for (barcode in result) {
val raw = barcode.rawValue ?: barcode.rawBytes?.let { String(it) }
val value = if (barcode.valueType == Barcode.TYPE_CONTACT_INFO) {
raw
} else {
barcode.displayValue
}
val cornerPoints = mutableListOf<Int>()
barcode.cornerPoints?.let { points ->
for (point in points) {
cornerPoints.addAll(listOf(point.x, point.y))
}
}
results.add(BarCodeScannerResult(barcode.format, value, raw, cornerPoints, inputImage.height, inputImage.width))
}
return@withContext results
} catch (e: Exception) {
Log.e(TAG, "Failed to detect barcode: " + e.message)
return@withContext emptyList()
}
}
fun setSettings(formats: List<Int>) {
if (areNewAndOldBarCodeTypesEqual(formats)) {
return
}
val barcodeFormats = formats.reduce { acc, it ->
acc or it
}
barCodeTypes = formats
barcodeScannerOptions = BarcodeScannerOptions.Builder()
.setBarcodeFormats(barcodeFormats)
.build()
barcodeScanner = BarcodeScanning.getClient(barcodeScannerOptions)
}
private fun areNewAndOldBarCodeTypesEqual(newBarCodeTypes: List<Int>): Boolean {
barCodeTypes?.run {
// create distinct-values sets
val prevTypesSet = toHashSet()
val nextTypesSet = newBarCodeTypes.toHashSet()
// sets sizes are equal -> possible content equality
if (prevTypesSet.size == nextTypesSet.size) {
prevTypesSet.removeAll(nextTypesSet)
// every element from new set was in previous one -> sets are equal
return prevTypesSet.isEmpty()
}
}
return false
}
companion object {
private val TAG = MLKitBarCodeScanner::class.java.simpleName
}
}
suspend fun <T> Task<T>.await(): T = suspendCancellableCoroutine { continuation ->
addOnSuccessListener { result ->
continuation.resume(result)
}
addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
addOnCanceledListener {
continuation.cancel()
}
}

View File

@@ -0,0 +1,23 @@
package expo.modules.camera.common
import android.os.Bundle
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
class BarcodeScannedEvent(
@Field val target: Int,
@Field val data: String,
@Field val raw: String,
@Field val type: String,
@Field val cornerPoints: ArrayList<Bundle>,
@Field val boundingBox: Bundle
) : Record
class CameraMountErrorEvent(
@Field val message: String
) : Record
class PictureSavedEvent(
@Field val id: Int,
@Field val data: Bundle
) : Record

View File

@@ -0,0 +1,7 @@
package expo.modules.camera.legacy
import expo.modules.kotlin.exception.CodedException
class CameraExceptions {
class CameraIsNotRunning : CodedException(message = "Camera is not running")
}

View File

@@ -0,0 +1,128 @@
package expo.modules.camera.legacy
import androidx.exifinterface.media.ExifInterface
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.media.CamcorderProfile
import android.os.Bundle
import com.google.android.cameraview.CameraView
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
object CameraViewHelper {
// Utilities
@JvmStatic
fun getCorrectCameraRotation(rotation: Int, facing: Int) =
if (facing == CameraView.FACING_FRONT) {
(rotation - 90 + 360) % 360
} else {
(-rotation + 90 + 360) % 360
}
@JvmStatic
fun getCamcorderProfile(cameraId: Int, quality: Int): CamcorderProfile {
var profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH)
when (quality) {
VIDEO_2160P -> profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P)
VIDEO_1080P -> profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P)
VIDEO_720P -> profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P)
VIDEO_480P -> profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P)
VIDEO_4x3 -> {
profile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P)
profile.videoFrameWidth = 640
}
}
return profile
}
@JvmStatic
fun getExifData(exifInterface: ExifInterface): Bundle {
val exifMap = Bundle()
for ((type, name) in exifTags) {
if (exifInterface.getAttribute(name) != null) {
when (type) {
"string" -> exifMap.putString(name, exifInterface.getAttribute(name))
"int" -> exifMap.putInt(name, exifInterface.getAttributeInt(name, 0))
"double" -> exifMap.putDouble(name, exifInterface.getAttributeDouble(name, 0.0))
}
}
}
exifInterface.latLong?.let {
exifMap.putDouble(ExifInterface.TAG_GPS_LATITUDE, it[0])
exifMap.putDouble(ExifInterface.TAG_GPS_LONGITUDE, it[1])
exifMap.putDouble(ExifInterface.TAG_GPS_ALTITUDE, exifInterface.getAltitude(0.0))
}
return exifMap
}
@JvmStatic
@Throws(IllegalArgumentException::class)
fun setExifData(baseExif: ExifInterface, exifMap: Map<String, Any>) {
for ((_, name) in exifTags) {
exifMap[name]?.let {
// Convert possible type to string before putting into baseExif
when (it) {
is String -> baseExif.setAttribute(name, it)
is Number -> baseExif.setAttribute(name, it.toDouble().toBigDecimal().toPlainString())
is Boolean -> baseExif.setAttribute(name, it.toString())
}
}
}
if (exifMap.containsKey(ExifInterface.TAG_GPS_LATITUDE) &&
exifMap.containsKey(ExifInterface.TAG_GPS_LONGITUDE) &&
exifMap[ExifInterface.TAG_GPS_LATITUDE] is Number &&
exifMap[ExifInterface.TAG_GPS_LONGITUDE] is Number
) {
baseExif.setLatLong(
exifMap[ExifInterface.TAG_GPS_LATITUDE] as Double,
exifMap[ExifInterface.TAG_GPS_LONGITUDE] as Double
)
}
if (exifMap.containsKey(ExifInterface.TAG_GPS_ALTITUDE) &&
exifMap[ExifInterface.TAG_GPS_ALTITUDE] is Number
) {
baseExif.setAltitude(exifMap[ExifInterface.TAG_GPS_ALTITUDE] as Double)
}
}
@JvmStatic
@Throws(IOException::class)
fun addExifData(baseExif: ExifInterface, additionalExif: ExifInterface) {
for (tagInfo in exifTags) {
val name = tagInfo[1]
additionalExif.getAttribute(name)?.let {
baseExif.setAttribute(name, it)
}
}
baseExif.saveAttributes()
}
fun generateSimulatorPhoto(width: Int, height: Int): ByteArray {
val fakePhotoBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(fakePhotoBitmap)
val background = Paint().apply {
color = Color.BLACK
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), background)
val textPaint = Paint().apply {
color = Color.YELLOW
textSize = 35f
}
val calendar = Calendar.getInstance()
val simpleDateFormat = SimpleDateFormat("dd.MM.yy HH:mm:ss", Locale.US)
canvas.drawText(simpleDateFormat.format(calendar.time), width * 0.1f, height * 0.9f, textPaint)
val stream = ByteArrayOutputStream()
fakePhotoBitmap.compress(Bitmap.CompressFormat.PNG, 90, stream)
val fakePhotoByteArray = stream.toByteArray()
return fakePhotoByteArray
}
}

View File

@@ -0,0 +1,265 @@
package expo.modules.camera.legacy
import android.Manifest
import com.google.android.cameraview.AspectRatio
import com.google.android.cameraview.Size
import expo.modules.camera.legacy.tasks.ResolveTakenPictureAsyncTask
import expo.modules.core.interfaces.services.UIManager
import expo.modules.core.utilities.EmulatorUtilities
import expo.modules.interfaces.barcodescanner.BarCodeScannerSettings
import expo.modules.interfaces.permissions.Permissions
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Queues
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.io.File
class CameraViewLegacyModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoCameraLegacy")
Constants(
"Type" to mapOf(
"front" to com.google.android.cameraview.Constants.FACING_FRONT,
"back" to com.google.android.cameraview.Constants.FACING_BACK
),
"FlashMode" to mapOf(
"off" to com.google.android.cameraview.Constants.FLASH_OFF,
"on" to com.google.android.cameraview.Constants.FLASH_ON,
"auto" to com.google.android.cameraview.Constants.FLASH_AUTO,
"torch" to com.google.android.cameraview.Constants.FLASH_TORCH
),
"AutoFocus" to mapOf(
"on" to true,
"off" to false
),
"WhiteBalance" to mapOf(
"auto" to com.google.android.cameraview.Constants.WB_AUTO,
"cloudy" to com.google.android.cameraview.Constants.WB_CLOUDY,
"sunny" to com.google.android.cameraview.Constants.WB_SUNNY,
"shadow" to com.google.android.cameraview.Constants.WB_SHADOW,
"fluorescent" to com.google.android.cameraview.Constants.WB_FLUORESCENT,
"incandescent" to com.google.android.cameraview.Constants.WB_INCANDESCENT
),
"VideoQuality" to mapOf(
"2160p" to VIDEO_2160P,
"1080p" to VIDEO_1080P,
"720p" to VIDEO_720P,
"480p" to VIDEO_480P,
"4:3" to VIDEO_4x3
)
)
AsyncFunction("pausePreview") { viewTag: Int ->
val view = findView(viewTag)
if (view.cameraView.isCameraOpened) {
view.cameraView.pausePreview()
}
}.runOnQueue(Queues.MAIN)
AsyncFunction("resumePreview") { viewTag: Int ->
val view = findView(viewTag)
if (view.cameraView.isCameraOpened) {
view.cameraView.resumePreview()
}
}.runOnQueue(Queues.MAIN)
AsyncFunction("takePicture") { options: PictureOptions, viewTag: Int, promise: Promise ->
val view = findView(viewTag)
if (!EmulatorUtilities.isRunningOnEmulator()) {
if (!view.cameraView.isCameraOpened) {
throw CameraExceptions.CameraIsNotRunning()
}
view.takePicture(options, promise, cacheDirectory)
} else {
val image = CameraViewHelper.generateSimulatorPhoto(view.width, view.height)
ResolveTakenPictureAsyncTask(image, promise, options, cacheDirectory, view).execute()
}
}.runOnQueue(Queues.MAIN)
AsyncFunction("record") { options: RecordingOptions, viewTag: Int, promise: Promise ->
if (!options.mute && !permissionsManager.hasGrantedPermissions(Manifest.permission.RECORD_AUDIO)) {
throw Exceptions.MissingPermissions(Manifest.permission.RECORD_AUDIO)
}
val view = findView(viewTag)
if (!view.cameraView.isCameraOpened) {
throw CameraExceptions.CameraIsNotRunning()
}
view.record(options, promise, cacheDirectory)
}.runOnQueue(Queues.MAIN)
AsyncFunction("stopRecording") { viewTag: Int ->
val view = findView(viewTag)
if (view.cameraView.isCameraOpened) {
view.cameraView.stopRecording()
}
}.runOnQueue(Queues.MAIN)
AsyncFunction("getSupportedRatios") { viewTag: Int ->
val view = findView(viewTag)
if (!view.cameraView.isCameraOpened) {
throw CameraExceptions.CameraIsNotRunning()
}
return@AsyncFunction view.cameraView.supportedAspectRatios.map { it.toString() }
}.runOnQueue(Queues.MAIN)
AsyncFunction("getAvailablePictureSizes") { ratio: String, viewTag: Int ->
val view = findView(viewTag)
if (!view.cameraView.isCameraOpened) {
throw CameraExceptions.CameraIsNotRunning()
}
val sizes = view.cameraView.getAvailablePictureSizes(AspectRatio.parse(ratio))
return@AsyncFunction sizes.map { it.toString() }
}.runOnQueue(Queues.MAIN)
AsyncFunction("requestPermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.CAMERA
)
}
AsyncFunction("requestCameraPermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.CAMERA
)
}
AsyncFunction("requestMicrophonePermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.RECORD_AUDIO
)
}
AsyncFunction("getPermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.CAMERA
)
}
AsyncFunction("getCameraPermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.CAMERA
)
}
AsyncFunction("getMicrophonePermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(
permissionsManager,
promise,
Manifest.permission.RECORD_AUDIO
)
}
View(ExpoCameraView::class) {
Events(
"onCameraReady",
"onMountError",
"onBarCodeScanned",
"onFacesDetected",
"onFaceDetectionError",
"onPictureSaved"
)
OnViewDestroys<ExpoCameraView> { view ->
val uiManager = appContext.legacyModule<UIManager>()
uiManager?.unregisterLifecycleEventListener(view)
view.cameraView.stop()
}
Prop("type") { view: ExpoCameraView, type: Int ->
view.cameraView.facing = type
}
Prop("ratio") { view: ExpoCameraView, ratio: String? ->
if (ratio == null) {
return@Prop
}
view.cameraView.setAspectRatio(AspectRatio.parse(ratio))
}
Prop("flashMode") { view: ExpoCameraView, torchMode: Int ->
view.cameraView.flash = torchMode
}
Prop("autoFocus") { view: ExpoCameraView, autoFocus: Boolean ->
view.cameraView.autoFocus = autoFocus
}
Prop("focusDepth") { view: ExpoCameraView, depth: Float ->
view.cameraView.focusDepth = depth
}
Prop("zoom") { view: ExpoCameraView, zoom: Float ->
view.cameraView.zoom = zoom
}
Prop("whiteBalance") { view: ExpoCameraView, whiteBalance: Int ->
view.cameraView.whiteBalance = whiteBalance
}
Prop("pictureSize") { view: ExpoCameraView, size: String? ->
if (size == null) {
return@Prop
}
view.cameraView.pictureSize = Size.parse(size)
}
Prop("barCodeScannerSettings") { view: ExpoCameraView, settings: Map<String, Any?>? ->
if (settings == null) {
return@Prop
}
view.setBarCodeScannerSettings(BarCodeScannerSettings(settings))
}
Prop("useCamera2Api") { view: ExpoCameraView, useCamera2Api: Boolean ->
view.cameraView.setUsingCamera2Api(useCamera2Api)
}
Prop("barCodeScannerEnabled") { view: ExpoCameraView, barCodeScannerEnabled: Boolean? ->
view.setShouldScanBarCodes(barCodeScannerEnabled ?: false)
}
Prop("faceDetectorEnabled") { view: ExpoCameraView, faceDetectorEnabled: Boolean? ->
view.setShouldDetectFaces(faceDetectorEnabled ?: false)
}
Prop("faceDetectorSettings") { view: ExpoCameraView, settings: Map<String, Any>? ->
view.setFaceDetectorSettings(settings)
}
}
}
private val cacheDirectory: File
get() = appContext.cacheDirectory
private val permissionsManager: Permissions
get() = appContext.permissions ?: throw Exceptions.PermissionsModuleNotFound()
private fun findView(viewTag: Int): ExpoCameraView {
return appContext.findView(viewTag)
?: throw Exceptions.ViewNotFound(ExpoCameraView::class, viewTag)
}
}

View File

@@ -0,0 +1,15 @@
package expo.modules.camera.legacy
import android.os.Bundle
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
data class FaceDetectionErrorEvent(
@Field val isOperational: Boolean
) : Record
data class FacesDetectedEvent(
@Field val type: String,
@Field val faces: List<Bundle>,
@Field val target: Int
) : Record

View File

@@ -0,0 +1,145 @@
package expo.modules.camera.legacy
import androidx.exifinterface.media.ExifInterface
const val VIDEO_2160P = 0
const val VIDEO_1080P = 1
const val VIDEO_720P = 2
const val VIDEO_480P = 3
const val VIDEO_4x3 = 4
val exifTags = arrayOf(
arrayOf("string", ExifInterface.TAG_ARTIST),
arrayOf("int", ExifInterface.TAG_BITS_PER_SAMPLE),
arrayOf("int", ExifInterface.TAG_COMPRESSION),
arrayOf("string", ExifInterface.TAG_COPYRIGHT),
arrayOf("string", ExifInterface.TAG_DATETIME),
arrayOf("string", ExifInterface.TAG_IMAGE_DESCRIPTION),
arrayOf("int", ExifInterface.TAG_IMAGE_LENGTH),
arrayOf("int", ExifInterface.TAG_IMAGE_WIDTH),
arrayOf("int", ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT),
arrayOf("int", ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH),
arrayOf("string", ExifInterface.TAG_MAKE),
arrayOf("string", ExifInterface.TAG_MODEL),
arrayOf("int", ExifInterface.TAG_ORIENTATION),
arrayOf("int", ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION),
arrayOf("int", ExifInterface.TAG_PLANAR_CONFIGURATION),
arrayOf("double", ExifInterface.TAG_PRIMARY_CHROMATICITIES),
arrayOf("double", ExifInterface.TAG_REFERENCE_BLACK_WHITE),
arrayOf("int", ExifInterface.TAG_RESOLUTION_UNIT),
arrayOf("int", ExifInterface.TAG_ROWS_PER_STRIP),
arrayOf("int", ExifInterface.TAG_SAMPLES_PER_PIXEL),
arrayOf("string", ExifInterface.TAG_SOFTWARE),
arrayOf("int", ExifInterface.TAG_STRIP_BYTE_COUNTS),
arrayOf("int", ExifInterface.TAG_STRIP_OFFSETS),
arrayOf("int", ExifInterface.TAG_TRANSFER_FUNCTION),
arrayOf("double", ExifInterface.TAG_WHITE_POINT),
arrayOf("double", ExifInterface.TAG_X_RESOLUTION),
arrayOf("double", ExifInterface.TAG_Y_CB_CR_COEFFICIENTS),
arrayOf("int", ExifInterface.TAG_Y_CB_CR_POSITIONING),
arrayOf("int", ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING),
arrayOf("double", ExifInterface.TAG_Y_RESOLUTION),
arrayOf("double", ExifInterface.TAG_APERTURE_VALUE),
arrayOf("double", ExifInterface.TAG_BRIGHTNESS_VALUE),
arrayOf("string", ExifInterface.TAG_CFA_PATTERN),
arrayOf("int", ExifInterface.TAG_COLOR_SPACE),
arrayOf("string", ExifInterface.TAG_COMPONENTS_CONFIGURATION),
arrayOf("double", ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL),
arrayOf("int", ExifInterface.TAG_CONTRAST),
arrayOf("int", ExifInterface.TAG_CUSTOM_RENDERED),
arrayOf("string", ExifInterface.TAG_DATETIME_DIGITIZED),
arrayOf("string", ExifInterface.TAG_DATETIME_ORIGINAL),
arrayOf("string", ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION),
arrayOf("double", ExifInterface.TAG_DIGITAL_ZOOM_RATIO),
arrayOf("string", ExifInterface.TAG_EXIF_VERSION),
arrayOf("double", ExifInterface.TAG_EXPOSURE_BIAS_VALUE),
arrayOf("double", ExifInterface.TAG_EXPOSURE_INDEX),
arrayOf("int", ExifInterface.TAG_EXPOSURE_MODE),
arrayOf("int", ExifInterface.TAG_EXPOSURE_PROGRAM),
arrayOf("double", ExifInterface.TAG_EXPOSURE_TIME),
arrayOf("double", ExifInterface.TAG_F_NUMBER),
arrayOf("string", ExifInterface.TAG_FILE_SOURCE),
arrayOf("int", ExifInterface.TAG_FLASH),
arrayOf("double", ExifInterface.TAG_FLASH_ENERGY),
arrayOf("string", ExifInterface.TAG_FLASHPIX_VERSION),
arrayOf("double", ExifInterface.TAG_FOCAL_LENGTH),
arrayOf("int", ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM),
arrayOf("int", ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT),
arrayOf("double", ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION),
arrayOf("double", ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION),
arrayOf("int", ExifInterface.TAG_GAIN_CONTROL),
arrayOf("int", ExifInterface.TAG_ISO_SPEED_RATINGS),
arrayOf("string", ExifInterface.TAG_IMAGE_UNIQUE_ID),
arrayOf("int", ExifInterface.TAG_LIGHT_SOURCE),
arrayOf("string", ExifInterface.TAG_MAKER_NOTE),
arrayOf("double", ExifInterface.TAG_MAX_APERTURE_VALUE),
arrayOf("int", ExifInterface.TAG_METERING_MODE),
arrayOf("int", ExifInterface.TAG_NEW_SUBFILE_TYPE),
arrayOf("string", ExifInterface.TAG_OECF),
arrayOf("int", ExifInterface.TAG_PIXEL_X_DIMENSION),
arrayOf("int", ExifInterface.TAG_PIXEL_Y_DIMENSION),
arrayOf("string", ExifInterface.TAG_RELATED_SOUND_FILE),
arrayOf("int", ExifInterface.TAG_SATURATION),
arrayOf("int", ExifInterface.TAG_SCENE_CAPTURE_TYPE),
arrayOf("string", ExifInterface.TAG_SCENE_TYPE),
arrayOf("int", ExifInterface.TAG_SENSING_METHOD),
arrayOf("int", ExifInterface.TAG_SHARPNESS),
arrayOf("double", ExifInterface.TAG_SHUTTER_SPEED_VALUE),
arrayOf("string", ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE),
arrayOf("string", ExifInterface.TAG_SPECTRAL_SENSITIVITY),
arrayOf("int", ExifInterface.TAG_SUBFILE_TYPE),
arrayOf("string", ExifInterface.TAG_SUBSEC_TIME),
arrayOf("string", ExifInterface.TAG_SUBSEC_TIME_DIGITIZED),
arrayOf("string", ExifInterface.TAG_SUBSEC_TIME_ORIGINAL),
arrayOf("int", ExifInterface.TAG_SUBJECT_AREA),
arrayOf("double", ExifInterface.TAG_SUBJECT_DISTANCE),
arrayOf("int", ExifInterface.TAG_SUBJECT_DISTANCE_RANGE),
arrayOf("int", ExifInterface.TAG_SUBJECT_LOCATION),
arrayOf("string", ExifInterface.TAG_USER_COMMENT),
arrayOf("int", ExifInterface.TAG_WHITE_BALANCE),
arrayOf("double", ExifInterface.TAG_GPS_ALTITUDE),
arrayOf("int", ExifInterface.TAG_GPS_ALTITUDE_REF),
arrayOf("string", ExifInterface.TAG_GPS_AREA_INFORMATION),
arrayOf("double", ExifInterface.TAG_GPS_DOP),
arrayOf("string", ExifInterface.TAG_GPS_DATESTAMP),
arrayOf("double", ExifInterface.TAG_GPS_DEST_BEARING),
arrayOf("string", ExifInterface.TAG_GPS_DEST_BEARING_REF),
arrayOf("double", ExifInterface.TAG_GPS_DEST_DISTANCE),
arrayOf("string", ExifInterface.TAG_GPS_DEST_DISTANCE_REF),
arrayOf("double", ExifInterface.TAG_GPS_DEST_LATITUDE),
arrayOf("string", ExifInterface.TAG_GPS_DEST_LATITUDE_REF),
arrayOf("double", ExifInterface.TAG_GPS_DEST_LONGITUDE),
arrayOf("string", ExifInterface.TAG_GPS_DEST_LONGITUDE_REF),
arrayOf("int", ExifInterface.TAG_GPS_DIFFERENTIAL),
arrayOf("string", ExifInterface.TAG_GPS_H_POSITIONING_ERROR),
arrayOf("double", ExifInterface.TAG_GPS_IMG_DIRECTION),
arrayOf("string", ExifInterface.TAG_GPS_IMG_DIRECTION_REF),
arrayOf("double", ExifInterface.TAG_GPS_LATITUDE),
arrayOf("string", ExifInterface.TAG_GPS_LATITUDE_REF),
arrayOf("double", ExifInterface.TAG_GPS_LONGITUDE),
arrayOf("string", ExifInterface.TAG_GPS_LONGITUDE_REF),
arrayOf("string", ExifInterface.TAG_GPS_MAP_DATUM),
arrayOf("string", ExifInterface.TAG_GPS_MEASURE_MODE),
arrayOf("string", ExifInterface.TAG_GPS_PROCESSING_METHOD),
arrayOf("string", ExifInterface.TAG_GPS_SATELLITES),
arrayOf("double", ExifInterface.TAG_GPS_SPEED),
arrayOf("string", ExifInterface.TAG_GPS_SPEED_REF),
arrayOf("string", ExifInterface.TAG_GPS_STATUS),
arrayOf("string", ExifInterface.TAG_GPS_TIMESTAMP),
arrayOf("double", ExifInterface.TAG_GPS_TRACK),
arrayOf("string", ExifInterface.TAG_GPS_TRACK_REF),
arrayOf("string", ExifInterface.TAG_GPS_VERSION_ID),
arrayOf("string", ExifInterface.TAG_INTEROPERABILITY_INDEX),
arrayOf("int", ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH),
arrayOf("int", ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH),
arrayOf("int", ExifInterface.TAG_DNG_VERSION),
arrayOf("int", ExifInterface.TAG_DEFAULT_CROP_SIZE),
arrayOf("int", ExifInterface.TAG_ORF_PREVIEW_IMAGE_START),
arrayOf("int", ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH),
arrayOf("int", ExifInterface.TAG_ORF_ASPECT_FRAME),
arrayOf("int", ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER),
arrayOf("int", ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER),
arrayOf("int", ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER),
arrayOf("int", ExifInterface.TAG_RW2_SENSOR_TOP_BORDER),
arrayOf("int", ExifInterface.TAG_RW2_ISO)
)

View File

@@ -0,0 +1,435 @@
package expo.modules.camera.legacy
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.SurfaceTexture
import android.net.Uri
import android.os.Bundle
import android.view.View
import com.google.android.cameraview.CameraView
import expo.modules.camera.legacy.CameraViewHelper.getCamcorderProfile
import expo.modules.camera.legacy.CameraViewHelper.getCorrectCameraRotation
import expo.modules.camera.legacy.tasks.BarCodeScannerAsyncTask
import expo.modules.camera.legacy.tasks.BarCodeScannerAsyncTaskDelegate
import expo.modules.camera.legacy.tasks.FaceDetectorAsyncTaskDelegate
import expo.modules.camera.legacy.tasks.FaceDetectorTask
import expo.modules.camera.legacy.tasks.PictureSavedDelegate
import expo.modules.camera.legacy.tasks.ResolveTakenPictureAsyncTask
import expo.modules.camera.legacy.utils.FileSystemUtils
import expo.modules.camera.legacy.utils.ImageDimensions
import expo.modules.core.interfaces.LifecycleEventListener
import expo.modules.core.interfaces.services.UIManager
import expo.modules.core.utilities.EmulatorUtilities
import expo.modules.interfaces.barcodescanner.BarCodeScannerInterface
import expo.modules.interfaces.barcodescanner.BarCodeScannerProviderInterface
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
import expo.modules.interfaces.barcodescanner.BarCodeScannerSettings
import expo.modules.interfaces.camera.CameraViewInterface
import expo.modules.interfaces.facedetector.FaceDetectorInterface
import expo.modules.interfaces.facedetector.FaceDetectorProviderInterface
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.Promise
import expo.modules.kotlin.views.ExpoView
import java.io.File
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import expo.modules.camera.utils.mapX
import expo.modules.camera.utils.mapY
import kotlin.math.roundToInt
import android.view.WindowManager
import expo.modules.camera.common.BarcodeScannedEvent
import expo.modules.camera.common.CameraMountErrorEvent
import expo.modules.camera.common.PictureSavedEvent
import expo.modules.camera.records.BarcodeType
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult.BoundingBox
import expo.modules.kotlin.viewevent.EventDispatcher
@SuppressLint("ViewConstructor")
class ExpoCameraView(
context: Context,
appContext: AppContext
) : ExpoView(context, appContext),
LifecycleEventListener,
BarCodeScannerAsyncTaskDelegate,
FaceDetectorAsyncTaskDelegate,
PictureSavedDelegate,
CameraViewInterface {
internal val cameraView = CameraView(context, true)
private val pictureTakenPromises: Queue<Promise> = ConcurrentLinkedQueue()
private val pictureTakenOptions: MutableMap<Promise, PictureOptions> = ConcurrentHashMap()
private val pictureTakenDirectories: MutableMap<Promise, File> = ConcurrentHashMap()
private var videoRecordedPromise: Promise? = null
private var isPaused = false
private var isNew = true
private val onCameraReady by EventDispatcher<Unit>()
private val onMountError by EventDispatcher<CameraMountErrorEvent>()
private val onBarCodeScanned by EventDispatcher<BarcodeScannedEvent>(
/**
* We want every distinct barcode to be reported to the JS listener.
* If we return some static value as a coalescing key there may be two barcode events
* containing two different barcodes waiting to be transmitted to JS
* that would get coalesced (because both of them would have the same coalescing key).
* So let's differentiate them with a hash of the contents (mod short's max value).
*/
coalescingKey = { event -> (event.data.hashCode() % Short.MAX_VALUE).toShort() }
)
private val onFacesDetected by EventDispatcher<FacesDetectedEvent>(
/**
* Should events about detected faces coalesce, the best strategy will be
* to ensure that events with different faces count are always being transmitted.
*/
coalescingKey = { event -> (event.faces.size % Short.MAX_VALUE).toShort() }
)
private val onFaceDetectionError by EventDispatcher<FaceDetectionErrorEvent>()
private val onPictureSaved by EventDispatcher<PictureSavedEvent>(
coalescingKey = { event ->
val uriHash = event.data.getString("uri")?.hashCode() ?: -1
(uriHash % Short.MAX_VALUE).toShort()
}
)
// Concurrency lock for scanners to avoid flooding the runtime
@Volatile
var barCodeScannerTaskLock = false
@Volatile
var faceDetectorTaskLock = false
// Scanning-related properties
private var barCodeScanner: BarCodeScannerInterface? = null
private var faceDetector: FaceDetectorInterface? = null
private var pendingFaceDetectorSettings: Map<String, Any>? = null
private var shouldDetectFaces = false
private var mShouldScanBarCodes = false
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val width = right - left
val height = bottom - top
cameraView.layout(0, 0, width, height)
cameraView.setBackgroundColor(Color.BLACK)
val preview = cameraView.view ?: return
preview.layout(0, 0, width, height)
}
override fun onViewAdded(child: View) {
// react adds children to containers at the beginning of children list and that moves pre-react added preview to the end of that list
// above would cause preview (TextureView that covers all available space) to be rendered at the top of children stack
// while we need this preview to be rendered last beneath all other children
// child is not preview
if (cameraView === child) {
return
}
// bring to front all non-preview children
val childrenToBeReordered = mutableListOf<View>()
for (i in 0 until this.childCount) {
val childView = getChildAt(i)
if (i == 0 && childView === cameraView) {
// preview is already first in children list - do not reorder anything
return
}
if (childView !== cameraView) {
childrenToBeReordered.add(childView)
}
}
for (childView in childrenToBeReordered) {
bringChildToFront(childView)
}
cameraView.requestLayout()
cameraView.invalidate()
}
fun takePicture(options: PictureOptions, promise: Promise, cacheDirectory: File) {
pictureTakenPromises.add(promise)
pictureTakenOptions[promise] = options
pictureTakenDirectories[promise] = cacheDirectory
try {
cameraView.takePicture()
} catch (e: Exception) {
pictureTakenPromises.remove(promise)
pictureTakenOptions.remove(promise)
pictureTakenDirectories.remove(promise)
throw e
}
}
override fun onPictureSaved(response: Bundle) {
onPictureSaved(PictureSavedEvent(response.getInt("id"), response.getBundle("data")!!))
}
fun record(options: RecordingOptions, promise: Promise, cacheDirectory: File) {
try {
val path = FileSystemUtils.generateOutputPath(cacheDirectory, "Camera", ".mp4")
val profile = getCamcorderProfile(cameraView.cameraId, options.quality)
options.videoBitrate?.let { profile.videoBitRate = it }
if (cameraView.record(path, options.maxDuration * 1000, options.maxFileSize, !options.mute, profile)) {
videoRecordedPromise = promise
} else {
promise.reject("E_RECORDING_FAILED", "Starting video recording failed. Another recording might be in progress.", null)
}
} catch (e: IOException) {
promise.reject("E_RECORDING_FAILED", "Starting video recording failed - could not create video file.", null)
}
}
/**
* Initialize the barcode scanner.
* Supports all iOS codes except [code138, code39mod43, itf14]
* Additionally supports [codabar, code128, maxicode, rss14, rssexpanded, upc_a, upc_ean]
*/
private fun initBarCodeScanner() {
val barCodeScannerProvider = appContext.legacyModule<BarCodeScannerProviderInterface>()
barCodeScanner = barCodeScannerProvider?.createBarCodeDetectorWithContext(context)
}
fun setShouldScanBarCodes(shouldScanBarCodes: Boolean) {
mShouldScanBarCodes = shouldScanBarCodes
cameraView.scanning = mShouldScanBarCodes || shouldDetectFaces
}
fun setBarCodeScannerSettings(settings: BarCodeScannerSettings) {
barCodeScanner?.setSettings(settings)
}
// Even = portrait, odd = landscape
private fun getDeviceOrientation() =
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation
private fun transformBarCodeScannerResultToViewCoordinates(barCode: BarCodeScannerResult) {
val cornerPoints = barCode.cornerPoints
// For some reason they're swapped, I don't know anymore...
val cameraWidth = barCode.referenceImageHeight
val cameraHeight = barCode.referenceImageWidth
val facingBack = cameraView.facing == CameraView.FACING_BACK
val facingFront = cameraView.facing == CameraView.FACING_FRONT
val portrait = getDeviceOrientation() % 2 == 0
val landscape = getDeviceOrientation() % 2 == 1
if (facingBack && portrait) {
cornerPoints.mapX { cameraWidth - cornerPoints[it] }
}
if (facingBack && landscape) {
cornerPoints.mapY { cameraHeight - cornerPoints[it] }
}
if (facingFront) {
cornerPoints.mapX { cameraWidth - cornerPoints[it] }
cornerPoints.mapY { cameraHeight - cornerPoints[it] }
}
val scaleX = width / cameraWidth.toDouble()
val scaleY = height / cameraHeight.toDouble()
cornerPoints.mapX {
(cornerPoints[it] * scaleX)
.roundToInt()
}
cornerPoints.mapY {
(cornerPoints[it] * scaleY)
.roundToInt()
}
barCode.cornerPoints = cornerPoints
}
private fun getCornerPointsAndBoundingBox(cornerPoints: List<Int>, boundingBox: BoundingBox): Pair<ArrayList<Bundle>, Bundle> {
val density = cameraView.resources.displayMetrics.density
val convertedCornerPoints = ArrayList<Bundle>()
for (i in cornerPoints.indices step 2) {
val y = cornerPoints[i].toFloat() / density
val x = cornerPoints[i + 1].toFloat() / density
convertedCornerPoints.add(
Bundle().apply {
putFloat("x", x)
putFloat("y", y)
}
)
}
val boundingBoxBundle = Bundle().apply {
putParcelable(
"origin",
Bundle().apply {
putFloat("x", boundingBox.x.toFloat() / density)
putFloat("y", boundingBox.y.toFloat() / density)
}
)
putParcelable(
"size",
Bundle().apply {
putFloat("width", boundingBox.width.toFloat() / density)
putFloat("height", boundingBox.height.toFloat() / density)
}
)
}
return convertedCornerPoints to boundingBoxBundle
}
override fun onBarCodeScanned(barCode: BarCodeScannerResult) {
if (mShouldScanBarCodes) {
transformBarCodeScannerResultToViewCoordinates(barCode)
val (cornerPoints, boundingBox) = getCornerPointsAndBoundingBox(barCode.cornerPoints, barCode.boundingBox)
onBarCodeScanned(
BarcodeScannedEvent(
target = id,
data = barCode.value,
raw = barCode.raw,
type = BarcodeType.mapFormatToString(barCode.type),
cornerPoints = cornerPoints,
boundingBox = boundingBox
)
)
}
}
override fun onBarCodeScanningTaskCompleted() {
barCodeScannerTaskLock = false
}
override fun setPreviewTexture(surfaceTexture: SurfaceTexture?) {
cameraView.setPreviewTexture(surfaceTexture)
}
override fun getPreviewSizeAsArray() = intArrayOf(cameraView.previewSize.width, cameraView.previewSize.height)
override fun onHostResume() {
if (hasCameraPermissions()) {
if (isPaused && !cameraView.isCameraOpened || isNew) {
isPaused = false
isNew = false
if (!EmulatorUtilities.isRunningOnEmulator()) {
cameraView.start()
val faceDetectorProvider = appContext.legacyModule<FaceDetectorProviderInterface>()
faceDetector = faceDetectorProvider?.createFaceDetectorWithContext(context)
pendingFaceDetectorSettings?.let {
faceDetector?.setSettings(it)
pendingFaceDetectorSettings = null
}
}
}
} else {
onMountError(CameraMountErrorEvent("Camera permissions not granted - component could not be rendered."))
}
}
override fun onHostPause() {
if (!isPaused && cameraView.isCameraOpened) {
faceDetector?.release()
isPaused = true
cameraView.stop()
}
}
override fun onHostDestroy() {
faceDetector?.release()
cameraView.stop()
}
private fun hasCameraPermissions(): Boolean {
val permissionsManager = appContext.permissions ?: return false
return permissionsManager.hasGrantedPermissions(Manifest.permission.CAMERA)
}
fun setShouldDetectFaces(shouldDetectFaces: Boolean) {
this.shouldDetectFaces = shouldDetectFaces
cameraView.scanning = mShouldScanBarCodes || shouldDetectFaces
}
fun setFaceDetectorSettings(settings: Map<String, Any>?) {
faceDetector?.setSettings(settings) ?: run {
pendingFaceDetectorSettings = settings
}
}
override fun onFacesDetected(faces: List<Bundle>) {
if (shouldDetectFaces) {
onFacesDetected(
FacesDetectedEvent(
"face",
faces,
id
)
)
}
}
override fun onFaceDetectionError(faceDetector: FaceDetectorInterface) {
faceDetectorTaskLock = false
if (shouldDetectFaces) {
onFaceDetectionError(FaceDetectionErrorEvent(true))
}
}
override fun onFaceDetectingTaskCompleted() {
faceDetectorTaskLock = false
}
init {
initBarCodeScanner()
isChildrenDrawingOrderEnabled = true
val uIManager = appContext.legacyModule<UIManager>()
uIManager!!.registerLifecycleEventListener(this)
cameraView.addCallback(object : CameraView.Callback() {
override fun onCameraOpened(cameraView: CameraView) {
onCameraReady(Unit)
}
override fun onMountError(cameraView: CameraView) {
onMountError(
CameraMountErrorEvent("Camera component could not be rendered - is there any other instance running?")
)
}
override fun onPictureTaken(cameraView: CameraView, data: ByteArray) {
val promise = pictureTakenPromises.poll() ?: return
val cacheDirectory = pictureTakenDirectories.remove(promise)
val options = pictureTakenOptions.remove(promise)!!
if (options.fastMode) {
promise.resolve(null)
}
cacheDirectory?.let {
ResolveTakenPictureAsyncTask(data, promise, options, it, this@ExpoCameraView).execute()
}
}
override fun onVideoRecorded(cameraView: CameraView, path: String) {
videoRecordedPromise?.let {
it.resolve(
Bundle().apply {
putString("uri", Uri.fromFile(File(path)).toString())
}
)
videoRecordedPromise = null
}
}
override fun onFramePreview(cameraView: CameraView, data: ByteArray, width: Int, height: Int, rotation: Int) {
val correctRotation = getCorrectCameraRotation(rotation, cameraView.facing)
if (mShouldScanBarCodes && !barCodeScannerTaskLock) {
barCodeScannerTaskLock = true
barCodeScanner?.let { BarCodeScannerAsyncTask(this@ExpoCameraView, it, data, width, height, rotation).execute() }
}
if (shouldDetectFaces && !faceDetectorTaskLock) {
faceDetectorTaskLock = true
val density = cameraView.resources.displayMetrics.density
val dimensions = ImageDimensions(width, height, correctRotation, cameraView.facing)
val scaleX = cameraView.width.toDouble() / (dimensions.width * density)
val scaleY = cameraView.height.toDouble() / (dimensions.height * density)
val task = faceDetector?.let { FaceDetectorTask(this@ExpoCameraView, it, data, width, height, correctRotation, cameraView.facing == CameraView.FACING_FRONT, scaleX, scaleY) }
task?.execute()
}
}
})
addView(cameraView)
}
}

View File

@@ -0,0 +1,35 @@
package expo.modules.camera.legacy
import android.media.CamcorderProfile
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
class PictureOptions : Record {
@Field val quality: Double = 1.0
@Field val base64: Boolean = false
@Field val exif: Boolean = false
@Field val additionalExif: Map<String, Any>? = null
@Field val skipProcessing: Boolean = false
@Field val fastMode: Boolean = false
@Field val id: Int? = null
@Field val maxDownsampling: Int = 1
}
class RecordingOptions : Record {
@Field val maxDuration: Int = 0
@Field val maxFileSize: Int = 0
@Field val quality: Int = CamcorderProfile.QUALITY_HIGH
@Field val mute: Boolean = false
@Field val videoBitrate: Int? = null
}

View File

@@ -0,0 +1,29 @@
package expo.modules.camera.legacy.tasks
import android.os.AsyncTask
import expo.modules.interfaces.barcodescanner.BarCodeScannerInterface
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
class BarCodeScannerAsyncTask(
private val delegate: BarCodeScannerAsyncTaskDelegate,
private val barCodeScanner: BarCodeScannerInterface,
private val imageData: ByteArray,
private val width: Int,
private val height: Int,
private val rotation: Int
) : AsyncTask<Void?, Void?, BarCodeScannerResult?>() {
override fun doInBackground(vararg params: Void?) = if (!isCancelled) {
barCodeScanner.scan(imageData, width, height, rotation)
} else {
null
}
override fun onPostExecute(result: BarCodeScannerResult?) {
super.onPostExecute(result)
result?.let {
delegate.onBarCodeScanned(result)
}
delegate.onBarCodeScanningTaskCompleted()
}
}

View File

@@ -0,0 +1,8 @@
package expo.modules.camera.legacy.tasks
import expo.modules.interfaces.barcodescanner.BarCodeScannerResult
interface BarCodeScannerAsyncTaskDelegate {
fun onBarCodeScanned(barCode: BarCodeScannerResult)
fun onBarCodeScanningTaskCompleted()
}

View File

@@ -0,0 +1,10 @@
package expo.modules.camera.legacy.tasks
import android.os.Bundle
import expo.modules.interfaces.facedetector.FaceDetectorInterface
interface FaceDetectorAsyncTaskDelegate {
fun onFacesDetected(faces: List<Bundle>)
fun onFaceDetectionError(faceDetector: FaceDetectorInterface)
fun onFaceDetectingTaskCompleted()
}

View File

@@ -0,0 +1,34 @@
package expo.modules.camera.legacy.tasks
import expo.modules.interfaces.facedetector.FaceDetectorInterface
class FaceDetectorTask(
private val mDelegate: FaceDetectorAsyncTaskDelegate,
private val mFaceDetector: FaceDetectorInterface,
private val mImageData: ByteArray,
private val mWidth: Int,
private val mHeight: Int,
private val mRotation: Int,
private val mMirrored: Boolean,
private val mScaleX: Double,
private val mScaleY: Double
) {
fun execute() {
mFaceDetector.detectFaces(
mImageData, mWidth, mHeight, mRotation, mMirrored, mScaleX, mScaleY,
{ result ->
result?.let {
mDelegate.onFacesDetected(result)
} ?: run {
mDelegate.onFaceDetectionError(mFaceDetector)
}
mDelegate.onFaceDetectingTaskCompleted()
},
{ error ->
mDelegate.onFaceDetectionError(mFaceDetector)
mDelegate.onFaceDetectingTaskCompleted()
},
{ skippedReason -> mDelegate.onFaceDetectingTaskCompleted() }
)
}
}

View File

@@ -0,0 +1,7 @@
package expo.modules.camera.legacy.tasks
import android.os.Bundle
interface PictureSavedDelegate {
fun onPictureSaved(response: Bundle)
}

View File

@@ -0,0 +1,244 @@
package expo.modules.camera.legacy.tasks
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import expo.modules.camera.legacy.PictureOptions
import expo.modules.camera.legacy.CameraViewHelper.addExifData
import expo.modules.camera.legacy.CameraViewHelper.getExifData
import expo.modules.camera.legacy.CameraViewHelper.setExifData
import expo.modules.camera.legacy.utils.FileSystemUtils
import expo.modules.kotlin.Promise
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
private const val DIRECTORY_NOT_FOUND_MSG = "Documents directory of the app could not be found."
private const val UNKNOWN_IO_EXCEPTION_MSG = "An unknown I/O exception has occurred."
private const val UNKNOWN_EXCEPTION_MSG = "An unknown exception has occurred."
private const val PARAMETER_EXCEPTION_MSG = "An incompatible parameter has been passed in. "
private const val OUT_OF_MEMORY_EXCEPTION_MSG = "Cannot allocate enough space to process the taken picture."
private const val ERROR_TAG = "E_TAKING_PICTURE_FAILED"
private const val OUT_OF_MEMORY_TAG = "ERR_CAMERA_OUT_OF_MEMORY"
private const val DIRECTORY_NAME = "Camera"
private const val EXTENSION = ".jpg"
private const val BASE64_KEY = "base64"
private const val HEIGHT_KEY = "height"
private const val WIDTH_KEY = "width"
private const val EXIF_KEY = "exif"
private const val DATA_KEY = "data"
private const val URI_KEY = "uri"
private const val ID_KEY = "id"
class ResolveTakenPictureAsyncTask(
private var imageData: ByteArray,
private var promise: Promise,
private var options: PictureOptions,
private val directory: File,
private var pictureSavedDelegate: PictureSavedDelegate
) : AsyncTask<Void?, Void?, Bundle?>() {
private val quality: Int
get() = (options.quality * 100).toInt()
override fun doInBackground(vararg params: Void?): Bundle? {
// handle SkipProcessing
if (options.skipProcessing) {
return handleSkipProcessing()
}
// set, read, and apply EXIF data
try {
ByteArrayInputStream(imageData).use { inputStream ->
val response = Bundle()
val exifInterface = ExifInterface(inputStream)
// If there are additional exif data, insert it here
options.additionalExif?.let {
setExifData(exifInterface, it)
}
// Get orientation of the image from mImageData via inputStream
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
val bitmapOptions = BitmapFactory
.Options()
.apply {
inSampleSize = 1
}
var bitmap: Bitmap? = null
var lastError: Error? = null
// If OOM exception was thrown, we try to use downsampling to recover.
while (bitmapOptions.inSampleSize <= options.maxDownsampling) {
try {
bitmap = decodeBitmap(imageData, orientation, options.exif, bitmapOptions)
break
} catch (exception: OutOfMemoryError) {
bitmapOptions.inSampleSize *= 2
lastError = exception
}
}
if (bitmap == null) {
promise.reject(OUT_OF_MEMORY_TAG, OUT_OF_MEMORY_EXCEPTION_MSG, lastError)
return null
}
// Write Exif data to the response if requested
if (options.exif) {
val exifData = getExifData(exifInterface)
response.putBundle(EXIF_KEY, exifData)
}
// Upon rotating, write the image's dimensions to the response
response.apply {
putInt(WIDTH_KEY, bitmap.width)
putInt(HEIGHT_KEY, bitmap.height)
}
// Cache compressed image in imageStream
ByteArrayOutputStream().use { imageStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, imageStream)
// Write compressed image to file in cache directory
val filePath = writeStreamToFile(imageStream)
// Save Exif data to the image if requested
if (options.exif) {
val exifFromFile = ExifInterface(filePath!!)
addExifData(exifFromFile, exifInterface)
}
val imageFile = File(filePath)
val fileUri = Uri.fromFile(imageFile).toString()
response.putString(URI_KEY, fileUri)
// Write base64-encoded image to the response if requested
if (options.base64) {
response.putString(BASE64_KEY, Base64.encodeToString(imageStream.toByteArray(), Base64.NO_WRAP))
}
}
return response
}
} catch (e: Exception) {
when (e) {
is Resources.NotFoundException -> promise.reject(ERROR_TAG, DIRECTORY_NOT_FOUND_MSG, e)
is IOException -> promise.reject(ERROR_TAG, UNKNOWN_IO_EXCEPTION_MSG, e)
is IllegalArgumentException -> promise.reject(ERROR_TAG, PARAMETER_EXCEPTION_MSG, e)
else -> promise.reject(ERROR_TAG, UNKNOWN_EXCEPTION_MSG, e)
}
e.printStackTrace()
}
// An exception had to occur, promise has already been rejected. Do not try to resolve it again.
return null
}
private fun handleSkipProcessing(): Bundle? {
try {
// save byte array (it's already a JPEG)
ByteArrayOutputStream().use { imageStream ->
imageStream.write(imageData)
// write compressed image to file in cache directory
val filePath = writeStreamToFile(imageStream)
val imageFile = File(filePath)
// handle image uri
val fileUri = Uri.fromFile(imageFile).toString()
// read exif information
val exifInterface = ExifInterface(filePath!!)
return Bundle().apply {
putString(URI_KEY, fileUri)
putInt(WIDTH_KEY, exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, -1))
putInt(HEIGHT_KEY, exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, -1))
// handle exif request
if (options.exif) {
val exifData = getExifData(exifInterface)
putBundle(EXIF_KEY, exifData)
}
// handle base64
if (options.base64) {
putString(BASE64_KEY, Base64.encodeToString(imageData, Base64.NO_WRAP))
}
}
}
} catch (e: Exception) {
if (e is IOException) {
promise.reject(ERROR_TAG, UNKNOWN_IO_EXCEPTION_MSG, e)
} else {
promise.reject(ERROR_TAG, UNKNOWN_EXCEPTION_MSG, e)
}
e.printStackTrace()
}
// error occurred
return null
}
override fun onPostExecute(response: Bundle?) {
super.onPostExecute(response)
// If the response is not null everything went well and we can resolve the promise.
if (response != null) {
if (options.fastMode) {
val wrapper = Bundle()
wrapper.putInt(ID_KEY, requireNotNull(options.id))
wrapper.putBundle(DATA_KEY, response)
pictureSavedDelegate.onPictureSaved(wrapper)
} else {
promise.resolve(response)
}
}
}
// Write stream to file in cache directory
@Throws(Exception::class)
private fun writeStreamToFile(inputStream: ByteArrayOutputStream): String? {
try {
val outputPath = FileSystemUtils.generateOutputPath(directory, DIRECTORY_NAME, EXTENSION)
FileOutputStream(outputPath).use { outputStream ->
inputStream.writeTo(outputStream)
}
return outputPath
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
private fun decodeBitmap(imageData: ByteArray, orientation: Int, exif: Boolean, bitmapOptions: BitmapFactory.Options): Bitmap {
// Rotate the bitmap to the proper orientation if needed
return if (!exif) {
decodeAndRotateBitmap(imageData, getImageRotation(orientation), bitmapOptions)
} else {
BitmapFactory.decodeByteArray(imageData, 0, imageData.size, bitmapOptions)
}
}
private fun decodeAndRotateBitmap(imageData: ByteArray, angle: Int, options: BitmapFactory.Options): Bitmap {
val source = BitmapFactory.decodeByteArray(imageData, 0, imageData.size, options)
val matrix = Matrix()
matrix.postRotate(angle.toFloat())
return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
}
// Get rotation degrees from Exif orientation enum
private fun getImageRotation(orientation: Int) = when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> 90
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_ROTATE_270 -> 270
else -> 0
}
}

View File

@@ -0,0 +1,31 @@
package expo.modules.camera.legacy.utils
import java.io.File
import java.io.IOException
import java.util.*
object FileSystemUtils {
@Throws(IOException::class)
fun ensureDirExists(dir: File): File {
if (!(dir.isDirectory || dir.mkdirs())) {
throw IOException("Couldn't create directory '$dir'")
}
return dir
}
@Throws(IOException::class)
fun generateOutputPath(internalDirectory: File, dirName: String, extension: String): String {
val directory = File(internalDirectory.toString() + File.separator + dirName)
ensureDirExists(directory)
val filename = UUID.randomUUID().toString()
return directory.toString() + File.separator + filename + extension
}
@Throws(IOException::class)
fun generateOutputFile(internalDirectory: File, dirName: String, extension: String): File {
val directory = File(internalDirectory.toString() + File.separator + dirName)
ensureDirExists(directory)
val filename = UUID.randomUUID().toString()
return File(directory.toString() + File.separator + filename + extension)
}
}

View File

@@ -0,0 +1,18 @@
package expo.modules.camera.legacy.utils
data class ImageDimensions @JvmOverloads constructor(private val mWidth: Int, private val mHeight: Int, val rotation: Int = 0, val facing: Int = -1) {
private val isLandscape: Boolean
get() = rotation % 180 == 90
val width: Int
get() = if (isLandscape) {
mHeight
} else {
mWidth
}
val height: Int
get() = if (isLandscape) {
mWidth
} else {
mHeight
}
}

View File

@@ -0,0 +1,138 @@
package expo.modules.camera.records
import android.hardware.camera2.CameraMetadata
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.video.Quality
import com.google.mlkit.vision.barcode.common.Barcode
import expo.modules.camera.CameraExceptions
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import expo.modules.kotlin.types.Enumerable
enum class CameraType(val value: String) : Enumerable {
FRONT("front"),
BACK("back");
fun mapToSelector() = when (this) {
FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA
BACK -> CameraSelector.DEFAULT_BACK_CAMERA
}
fun mapToCharacteristic() = when (this) {
FRONT -> CameraMetadata.LENS_FACING_FRONT
BACK -> CameraMetadata.LENS_FACING_BACK
}
}
enum class CameraRatio(val value: String) : Enumerable {
FOUR_THREE("4:3"),
SIXTEEN_NINE("16:9"),
ONE_ONE("1:1");
fun mapToStrategy() = when (this) {
FOUR_THREE -> AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY
SIXTEEN_NINE -> AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
else -> throw CameraExceptions.UnsupportedAspectRatioException(this.value)
}
}
enum class VideoQuality(val value: String) : Enumerable {
VIDEO2160P("2160p"),
VIDEO1080P("1080p"),
VIDEO720P("720p"),
VIDEO480P("480p"),
VIDEO4X3("4:3");
fun mapToQuality(): Quality = when (this) {
VIDEO2160P -> Quality.UHD
VIDEO1080P -> Quality.FHD
VIDEO720P -> Quality.HD
VIDEO480P -> Quality.SD
VIDEO4X3 -> Quality.LOWEST
}
}
enum class FlashMode(val value: String) : Enumerable {
AUTO("auto"),
ON("on"),
OFF("off");
fun mapToLens() = when (this) {
AUTO -> ImageCapture.FLASH_MODE_AUTO
OFF -> ImageCapture.FLASH_MODE_OFF
ON -> ImageCapture.FLASH_MODE_ON
}
}
enum class CameraMode(val value: String) : Enumerable {
PICTURE("picture"),
VIDEO("video")
}
enum class FocusMode(val value: String) : Enumerable {
ON("on"),
OFF("off")
}
data class BarcodeSettings(
@Field val barcodeTypes: List<BarcodeType>
) : Record
enum class BarcodeType(private val value: String) : Enumerable {
AZTEC("aztec"),
EAN13("ean13"),
EAN8("ean8"),
QR("qr"),
PDF417("pdf417"),
UPCE("upc_e"),
DATAMATRIX("datamatrix"),
CODE39("code39"),
CODE93("code93"),
ITF14("itf14"),
CODABAR("codabar"),
CODE128("code128"),
UPCA("upc_a"),
UNKNOWN("unknown");
fun mapToBarcode() = when (this) {
AZTEC -> Barcode.FORMAT_AZTEC
EAN13 -> Barcode.FORMAT_EAN_13
EAN8 -> Barcode.FORMAT_EAN_8
QR -> Barcode.FORMAT_QR_CODE
PDF417 -> Barcode.FORMAT_PDF417
UPCE -> Barcode.FORMAT_UPC_E
DATAMATRIX -> Barcode.FORMAT_DATA_MATRIX
CODE39 -> Barcode.FORMAT_CODE_39
CODE93 -> Barcode.FORMAT_CODE_93
ITF14 -> Barcode.FORMAT_ITF
CODABAR -> Barcode.FORMAT_CODABAR
CODE128 -> Barcode.FORMAT_CODE_128
UPCA -> Barcode.FORMAT_UPC_A
UNKNOWN -> Barcode.FORMAT_UNKNOWN
}
companion object {
fun mapFormatToString(format: Int): String {
val result = when (format) {
Barcode.FORMAT_AZTEC -> AZTEC
Barcode.FORMAT_EAN_13 -> EAN13
Barcode.FORMAT_EAN_8 -> EAN8
Barcode.FORMAT_QR_CODE -> QR
Barcode.FORMAT_PDF417 -> PDF417
Barcode.FORMAT_UPC_E -> UPCE
Barcode.FORMAT_DATA_MATRIX -> DATAMATRIX
Barcode.FORMAT_CODE_39 -> CODE39
Barcode.FORMAT_CODE_93 -> CODE93
Barcode.FORMAT_ITF -> ITF14
Barcode.FORMAT_CODABAR -> CODABAR
Barcode.FORMAT_CODE_128 -> CODE128
Barcode.FORMAT_UPC_A -> UPCA
else -> UNKNOWN
}
return result.value
}
}
}

View File

@@ -0,0 +1,7 @@
package expo.modules.camera.tasks
import android.os.Bundle
fun interface PictureSavedDelegate {
fun onPictureSaved(response: Bundle)
}

View File

@@ -0,0 +1,273 @@
package expo.modules.camera.tasks
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import expo.modules.camera.PictureOptions
import expo.modules.camera.legacy.CameraViewHelper.addExifData
import expo.modules.camera.legacy.CameraViewHelper.getExifData
import expo.modules.camera.legacy.CameraViewHelper.setExifData
import expo.modules.camera.legacy.utils.FileSystemUtils
import expo.modules.kotlin.Promise
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
private const val DIRECTORY_NOT_FOUND_MSG = "Documents directory of the app could not be found."
private const val UNKNOWN_IO_EXCEPTION_MSG = "An unknown I/O exception has occurred."
private const val UNKNOWN_EXCEPTION_MSG = "An unknown exception has occurred."
private const val PARAMETER_EXCEPTION_MSG = "An incompatible parameter has been passed in. "
private const val OUT_OF_MEMORY_EXCEPTION_MSG = "Cannot allocate enough space to process the taken picture."
private const val ERROR_TAG = "E_TAKING_PICTURE_FAILED"
private const val OUT_OF_MEMORY_TAG = "ERR_CAMERA_OUT_OF_MEMORY"
private const val DIRECTORY_NAME = "Camera"
private const val EXTENSION = ".jpg"
private const val BASE64_KEY = "base64"
private const val HEIGHT_KEY = "height"
private const val WIDTH_KEY = "width"
private const val EXIF_KEY = "exif"
private const val DATA_KEY = "data"
private const val URI_KEY = "uri"
private const val ID_KEY = "id"
fun getMirroredOrientation(orientation: Int): Int {
return when (orientation) {
ExifInterface.ORIENTATION_NORMAL -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL
ExifInterface.ORIENTATION_ROTATE_90 -> ExifInterface.ORIENTATION_TRANSPOSE
ExifInterface.ORIENTATION_ROTATE_180 -> ExifInterface.ORIENTATION_FLIP_VERTICAL
ExifInterface.ORIENTATION_ROTATE_270 -> ExifInterface.ORIENTATION_TRANSVERSE
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> ExifInterface.ORIENTATION_NORMAL
ExifInterface.ORIENTATION_TRANSPOSE -> ExifInterface.ORIENTATION_ROTATE_90
ExifInterface.ORIENTATION_FLIP_VERTICAL -> ExifInterface.ORIENTATION_ROTATE_180
ExifInterface.ORIENTATION_TRANSVERSE -> ExifInterface.ORIENTATION_ROTATE_270
else -> ExifInterface.ORIENTATION_UNDEFINED
}
}
class ResolveTakenPicture(
private var imageData: ByteArray,
private var promise: Promise,
private var options: PictureOptions,
private var mirror: Boolean,
private val directory: File,
private var pictureSavedDelegate: PictureSavedDelegate
) {
private val quality: Int
get() = (options.quality * 100).toInt()
suspend fun resolve() = withContext(Dispatchers.IO) {
val bundle = processImage()
onComplete(bundle)
}
private fun processImage(): Bundle? {
// handle SkipProcessing
if (options.skipProcessing) {
return skipProcessing()
}
// set, read, and apply EXIF data
try {
ByteArrayInputStream(imageData).use { inputStream ->
val response = Bundle()
val exifInterface = ExifInterface(inputStream)
// If there are additional exif data, insert it here
options.additionalExif?.let {
setExifData(exifInterface, it)
}
// Get orientation of the image from mImageData via inputStream
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
if (mirror) {
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, getMirroredOrientation(orientation).toString())
}
val bitmapOptions = BitmapFactory
.Options()
.apply {
inSampleSize = 1
}
var bitmap: Bitmap? = null
var lastError: Error? = null
// If OOM exception was thrown, we try to use downsampling to recover.
while (bitmapOptions.inSampleSize <= options.maxDownsampling) {
try {
bitmap = decodeBitmap(imageData, orientation, options, bitmapOptions)
break
} catch (exception: OutOfMemoryError) {
bitmapOptions.inSampleSize *= 2
lastError = exception
}
}
if (bitmap == null) {
promise.reject(OUT_OF_MEMORY_TAG, OUT_OF_MEMORY_EXCEPTION_MSG, lastError)
return null
}
// Write Exif data to the response if requested
if (options.exif) {
val exifData = getExifData(exifInterface)
response.putBundle(EXIF_KEY, exifData)
}
// Upon rotating, write the image's dimensions to the response
response.apply {
putInt(WIDTH_KEY, bitmap.width)
putInt(HEIGHT_KEY, bitmap.height)
}
// Cache compressed image in imageStream
ByteArrayOutputStream().use { imageStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, imageStream)
// Write compressed image to file in cache directory
val filePath = writeStreamToFile(imageStream)
bitmap.recycle()
// Save Exif data to the image if requested
if (options.exif) {
val exifFromFile = ExifInterface(filePath!!)
addExifData(exifFromFile, exifInterface)
}
val imageFile = File(filePath)
val fileUri = Uri.fromFile(imageFile).toString()
response.putString(URI_KEY, fileUri)
// Write base64-encoded image to the response if requested
if (options.base64) {
response.putString(BASE64_KEY, Base64.encodeToString(imageStream.toByteArray(), Base64.NO_WRAP))
}
}
return response
}
} catch (e: Exception) {
when (e) {
is Resources.NotFoundException -> promise.reject(ERROR_TAG, DIRECTORY_NOT_FOUND_MSG, e)
is IOException -> promise.reject(ERROR_TAG, UNKNOWN_IO_EXCEPTION_MSG, e)
is IllegalArgumentException -> promise.reject(ERROR_TAG, PARAMETER_EXCEPTION_MSG, e)
else -> promise.reject(ERROR_TAG, UNKNOWN_EXCEPTION_MSG, e)
}
e.printStackTrace()
}
// An exception had to occur, promise has already been rejected. Do not try to resolve it again.
return null
}
private fun skipProcessing(): Bundle? {
try {
// save byte array (it's already a JPEG)
ByteArrayOutputStream().use { imageStream ->
imageStream.write(imageData)
// write compressed image to file in cache directory
val filePath = writeStreamToFile(imageStream)
val imageFile = filePath?.let { File(it) }
// handle image uri
val fileUri = Uri.fromFile(imageFile).toString()
// read exif information
val exifInterface = ExifInterface(filePath!!)
return Bundle().apply {
putString(URI_KEY, fileUri)
putInt(WIDTH_KEY, exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, -1))
putInt(HEIGHT_KEY, exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, -1))
// handle exif request
if (options.exif) {
val exifData = getExifData(exifInterface)
putBundle(EXIF_KEY, exifData)
}
// handle base64
if (options.base64) {
putString(BASE64_KEY, Base64.encodeToString(imageData, Base64.NO_WRAP))
}
}
}
} catch (e: IOException) {
promise.reject(ERROR_TAG, UNKNOWN_IO_EXCEPTION_MSG, e)
e.printStackTrace()
} catch (e: Exception) {
promise.reject(ERROR_TAG, UNKNOWN_EXCEPTION_MSG, e)
e.printStackTrace()
}
// error occurred
return null
}
private fun onComplete(response: Bundle?) {
if (response == null) {
return
}
if (options.fastMode) {
val wrapper = Bundle()
wrapper.putInt(ID_KEY, requireNotNull(options.id))
wrapper.putBundle(DATA_KEY, response)
pictureSavedDelegate.onPictureSaved(wrapper)
} else {
promise.resolve(response)
}
}
// Write stream to file in cache directory
@Throws(Exception::class)
private fun writeStreamToFile(inputStream: ByteArrayOutputStream): String? {
try {
val outputPath = FileSystemUtils.generateOutputPath(directory, DIRECTORY_NAME, EXTENSION)
FileOutputStream(outputPath).use { outputStream ->
inputStream.writeTo(outputStream)
}
return outputPath
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
private fun decodeBitmap(imageData: ByteArray, orientation: Int, options: PictureOptions, bitmapOptions: BitmapFactory.Options): Bitmap {
// Rotate the bitmap to the proper orientation if needed
return if (!options.exif) {
decodeAndRotateBitmap(imageData, getImageRotation(orientation), bitmapOptions)
} else {
BitmapFactory.decodeByteArray(imageData, 0, imageData.size, bitmapOptions)
}
}
private fun decodeAndRotateBitmap(imageData: ByteArray, angle: Int, options: BitmapFactory.Options): Bitmap {
val source = BitmapFactory.decodeByteArray(imageData, 0, imageData.size, options)
val matrix = Matrix()
matrix.apply {
postRotate(angle.toFloat())
if (mirror) {
postScale(-1f, 1f)
}
}
return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
}
// Get rotation degrees from Exif orientation enum
private fun getImageRotation(orientation: Int) = when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> 90
ExifInterface.ORIENTATION_TRANSPOSE -> 90
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_FLIP_VERTICAL -> 180
ExifInterface.ORIENTATION_ROTATE_270 -> 270
ExifInterface.ORIENTATION_TRANSVERSE -> 270
else -> 0
}
}

View File

@@ -0,0 +1,13 @@
package expo.modules.camera.utils
inline fun MutableList<Int>.mapY(block: (it: Int) -> Int) {
for (it in 0 until this.size step 2) {
this[it] = block(it)
}
}
inline fun MutableList<Int>.mapX(block: (it: Int) -> Int) {
for (it in 1 until this.size step 2) {
this[it] = block(it)
}
}

View File

@@ -0,0 +1,23 @@
package expo.modules.camera.utils
import java.io.File
import java.io.IOException
import java.util.*
object FileSystemUtils {
@Throws(IOException::class)
fun ensureDirExists(dir: File): File {
if (!(dir.isDirectory || dir.mkdirs())) {
throw IOException("Couldn't create directory '$dir'")
}
return dir
}
@Throws(IOException::class)
fun generateOutputFile(internalDirectory: File, dirName: String, extension: String): File {
val directory = File(internalDirectory.toString() + File.separator + dirName)
ensureDirExists(directory)
val filename = UUID.randomUUID().toString()
return File(directory.toString() + File.separator + filename + extension)
}
}

View File

@@ -0,0 +1,20 @@
package expo.modules.camera.utils
import expo.modules.camera.records.CameraType
data class ImageDimensions @JvmOverloads constructor(private val mWidth: Int, private val mHeight: Int, val rotation: Int = 0, val facing: CameraType = CameraType.BACK) {
private val isLandscape: Boolean
get() = rotation % 180 == 90
val width: Int
get() = if (isLandscape) {
mHeight
} else {
mWidth
}
val height: Int
get() = if (isLandscape) {
mWidth
} else {
mHeight
}
}