mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-20 23:11:24 +02:00
feat: add torch plugin for iOS and Android (#3551)
* feat(barcode-scanner): add torch toggle * feat: torch plugin * fix --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
c91a9e6af5
commit
d80b1be257
48 files changed
+1861
-4
No files matched your search
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,44 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.tauri.torch"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
|
||||
// Only compiled against: CameraX is on the runtime classpath when the barcode scanner plugin is,
|
||||
// and ScannerCamera, the only class using it, is only constructed while a scan is active.
|
||||
compileOnly("androidx.camera:camera-core:1.5.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.torch
|
||||
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.TorchState
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* The camera held by an active barcode scan.
|
||||
*
|
||||
* While CameraX holds the camera, `CameraManager.setTorchMode` fails with `CAMERA_IN_USE`, so the
|
||||
* torch has to go through the camera's own `CameraControl`. This is the only class referencing
|
||||
* CameraX, which is only on the classpath when the barcode scanner plugin is; only construct it
|
||||
* from a camera returned by [TorchPlugin.findScannerCamera].
|
||||
*/
|
||||
internal class ScannerCamera(camera: Any) {
|
||||
private val camera = camera as Camera
|
||||
|
||||
val hasFlashUnit: Boolean
|
||||
get() = camera.cameraInfo.hasFlashUnit()
|
||||
|
||||
val torchEnabled: Boolean
|
||||
get() = camera.cameraInfo.torchState.value == TorchState.ON
|
||||
|
||||
/** Calls [callback] with `null` once the torch changed, or with the error that prevented it. */
|
||||
fun setTorch(enabled: Boolean, executor: Executor, callback: (Throwable?) -> Unit) {
|
||||
val future = camera.cameraControl.enableTorch(enabled)
|
||||
future.addListener({
|
||||
try {
|
||||
future.get()
|
||||
callback(null)
|
||||
} catch (e: ExecutionException) {
|
||||
callback(e.cause ?: e)
|
||||
} catch (e: Exception) {
|
||||
callback(e)
|
||||
}
|
||||
}, executor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.torch
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraAccessException
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.tauri.Logger
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
|
||||
private const val ERROR_NOT_AVAILABLE = "The torch is not available on this device"
|
||||
private const val SCANNER_PLUGIN_CLASS = "app.tauri.barcodescanner.BarcodeScannerPlugin"
|
||||
|
||||
@TauriPlugin
|
||||
class TorchPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
private val cameraManager =
|
||||
activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
// the camera whose flash unit is used as the torch, or null if the device has none
|
||||
private val torchCameraId: String? = findTorchCameraId()
|
||||
|
||||
// the system-reported state of the torch, kept in sync by torchCallback so changes made by
|
||||
// the system or other apps are reflected
|
||||
@Volatile
|
||||
private var torchEnabled = false
|
||||
|
||||
private val torchCallback = object : CameraManager.TorchCallback() {
|
||||
override fun onTorchModeChanged(cameraId: String, enabled: Boolean) {
|
||||
if (cameraId == torchCameraId) torchEnabled = enabled
|
||||
}
|
||||
|
||||
override fun onTorchModeUnavailable(cameraId: String) {
|
||||
if (cameraId == torchCameraId) torchEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (torchCameraId != null) {
|
||||
cameraManager.registerTorchCallback(torchCallback, Handler(Looper.getMainLooper()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(activity: AppCompatActivity) {
|
||||
cameraManager.unregisterTorchCallback(torchCallback)
|
||||
}
|
||||
|
||||
private fun findTorchCameraId(): String? {
|
||||
return try {
|
||||
val withFlash = cameraManager.cameraIdList.filter {
|
||||
cameraManager.getCameraCharacteristics(it)
|
||||
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
|
||||
}
|
||||
withFlash.firstOrNull {
|
||||
cameraManager.getCameraCharacteristics(it)
|
||||
.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
|
||||
} ?: withFlash.firstOrNull()
|
||||
} catch (e: Exception) {
|
||||
Logger.error("Failed to look up the camera flash unit: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// The camera of an active barcode scan, if the barcode scanner plugin is installed and scanning.
|
||||
private fun findScannerCamera(): ScannerCamera? {
|
||||
val camera = try {
|
||||
Class.forName(SCANNER_PLUGIN_CLASS).getMethod("getActiveCamera").invoke(null)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return null
|
||||
return ScannerCamera(camera)
|
||||
}
|
||||
|
||||
private fun isTorchOn(): Boolean = findScannerCamera()?.torchEnabled ?: torchEnabled
|
||||
|
||||
private fun setTorch(invoke: Invoke, enabled: Boolean, onSuccess: () -> Unit) {
|
||||
val action = if (enabled) "enable" else "disable"
|
||||
|
||||
val scannerCamera = findScannerCamera()
|
||||
if (scannerCamera != null) {
|
||||
if (!scannerCamera.hasFlashUnit) {
|
||||
invoke.reject("The camera used by the barcode scanner has no flash unit")
|
||||
return
|
||||
}
|
||||
scannerCamera.setTorch(enabled, ContextCompat.getMainExecutor(activity)) { error ->
|
||||
if (error == null) {
|
||||
onSuccess()
|
||||
} else {
|
||||
invoke.reject("Failed to $action the torch: ${error.message}")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val cameraId = torchCameraId
|
||||
if (cameraId == null) {
|
||||
invoke.reject(ERROR_NOT_AVAILABLE)
|
||||
return
|
||||
}
|
||||
try {
|
||||
cameraManager.setTorchMode(cameraId, enabled)
|
||||
torchEnabled = enabled
|
||||
onSuccess()
|
||||
} catch (e: CameraAccessException) {
|
||||
when (e.reason) {
|
||||
CameraAccessException.CAMERA_IN_USE, CameraAccessException.MAX_CAMERAS_IN_USE ->
|
||||
invoke.reject("Failed to $action the torch: the camera is in use by another app")
|
||||
else -> invoke.reject("Failed to $action the torch: ${e.message}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Failed to $action the torch: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun enable(invoke: Invoke) {
|
||||
setTorch(invoke, true) { invoke.resolve() }
|
||||
}
|
||||
|
||||
@Command
|
||||
fun disable(invoke: Invoke) {
|
||||
setTorch(invoke, false) { invoke.resolve() }
|
||||
}
|
||||
|
||||
@Command
|
||||
fun toggle(invoke: Invoke) {
|
||||
val enabled = !isTorchOn()
|
||||
setTorch(invoke, enabled) {
|
||||
val result = JSObject()
|
||||
result.put("enabled", enabled)
|
||||
invoke.resolve(result)
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun isAvailable(invoke: Invoke) {
|
||||
val result = JSObject()
|
||||
result.put("available", torchCameraId != null)
|
||||
invoke.resolve(result)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun isEnabled(invoke: Invoke) {
|
||||
val result = JSObject()
|
||||
result.put("enabled", isTorchOn())
|
||||
invoke.resolve(result)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user