Devops: KTLint to lint Kotlin code (#6)

* Adds KTLint as a GitHub action
* Adds KTLint to the gradle project for IDE integration
* Adds .editorconfig to configure KTLint (android/)
This commit is contained in:
Marc Rousavy 2021-02-26 10:56:20 +01:00 committed by GitHub
parent 2e60110070
commit 03b9246afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1233 additions and 1190 deletions

View File

@ -7,10 +7,12 @@ on:
paths:
- '.github/workflows/validate-android.yml'
- 'android/**'
- '.editorconfig'
pull_request:
paths:
- '.github/workflows/validate-android.yml'
- 'android/**'
- '.editorconfig'
jobs:
lint:
@ -32,3 +34,12 @@ jobs:
- uses: yutailang0119/action-android-lint@v1.0.2
with:
xml_path: android/build/reports/lint-results.xml
ktlint:
name: Kotlin Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run KTLint
uses: mrousavy/action-ktlint@v1.6
with:
github_token: ${{ secrets.github_token }}

5
android/.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{kt,kts}]
indent_size=2
insert_final_newline=true
max_line_length=off
disabled_rules=no-wildcard-imports

16
android/README.md Normal file
View File

@ -0,0 +1,16 @@
# android
This folder contains the Android-platform-specific code for react-native-vision-camera.
## Prerequesites
1. Install ktlint
```sh
brew install ktlint
```
## Getting Started
It is recommended that you work on the code using the Example project (`example/android/`), since that always includes the React Native header files, plus you can easily test changes that way.
You can however still edit the library project here by opening this folder with Android Studio.

View File

@ -15,12 +15,15 @@ buildscript {
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
// ktlint
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['VisionCamera_' + name]

View File

@ -2,8 +2,8 @@ package com.mrousavy.camera
import android.annotation.SuppressLint
import androidx.camera.core.VideoCapture
import com.mrousavy.camera.utils.makeErrorMap
import com.facebook.react.bridge.*
import com.mrousavy.camera.utils.makeErrorMap
import kotlinx.coroutines.*
import java.io.File
@ -26,7 +26,9 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
}
val videoFileOptions = VideoCapture.OutputFileOptions.Builder(videoFile)
videoCapture!!.startRecording(videoFileOptions.build(), recordVideoExecutor, object : VideoCapture.OnVideoSavedCallback {
videoCapture!!.startRecording(
videoFileOptions.build(), recordVideoExecutor,
object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
val map = Arguments.createMap()
map.putString("path", videoFile.absolutePath)
@ -52,12 +54,12 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
// reset the torch mode
camera!!.cameraControl.enableTorch(torch == "on")
}
})
}
)
return TemporaryFile(videoFile.absolutePath)
}
@SuppressLint("RestrictedApi")
fun CameraView.stopRecording() {
if (videoCapture == null) {

View File

@ -7,10 +7,10 @@ import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageProxy
import androidx.exifinterface.media.ExifInterface
import com.mrousavy.camera.utils.*
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.mrousavy.camera.utils.*
import kotlinx.coroutines.*
import java.io.File
@ -70,7 +70,8 @@ suspend fun CameraView.takePhoto(options: ReadableMap): WritableMap = coroutineS
async(Dispatchers.IO) {
Log.d(CameraView.REACT_CLASS, "Creating temp file...")
File.createTempFile("mrousavy", ".jpg", context.cacheDir).apply { deleteOnExit() }
})
}
)
val photo = results.first { it is ImageProxy } as ImageProxy
val file = results.first { it is File } as File

View File

@ -2,10 +2,10 @@ package com.mrousavy.camera
import android.graphics.Bitmap
import androidx.exifinterface.media.ExifInterface
import com.mrousavy.camera.utils.buildMetadataMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.mrousavy.camera.utils.buildMetadataMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext

View File

@ -20,9 +20,9 @@ import androidx.camera.extensions.NightPreviewExtender
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.*
import com.mrousavy.camera.utils.*
import com.facebook.react.bridge.*
import com.facebook.react.uimanager.events.RCTEventEmitter
import com.mrousavy.camera.utils.*
import kotlinx.coroutines.*
import java.lang.IllegalArgumentException
import java.util.concurrent.Executors
@ -342,7 +342,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
Log.d(REACT_CLASS, "Session configured! Camera: ${camera!!}")
invokeOnInitialized()
} catch(exc: Throwable) {
} catch (exc: Throwable) {
throw when (exc) {
is CameraError -> exc
is IllegalArgumentException -> InvalidCameraDeviceError(exc)

View File

@ -1,14 +1,12 @@
package com.mrousavy.camera
import android.util.Log
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import java.lang.ref.WeakReference
class CameraViewManager : SimpleViewManager<CameraView>() {
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) {

View File

@ -14,11 +14,11 @@ import androidx.camera.core.ImageCapture
import androidx.camera.extensions.HdrImageCaptureExtender
import androidx.camera.extensions.NightImageCaptureExtender
import androidx.core.content.ContextCompat
import com.mrousavy.camera.parsers.*
import com.mrousavy.camera.utils.*
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
import com.mrousavy.camera.parsers.*
import com.mrousavy.camera.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -29,7 +29,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
var RequestCode = 10
fun parsePermissionStatus(status: Int): String {
return when(status) {
return when (status) {
PackageManager.PERMISSION_DENIED -> "denied"
PackageManager.PERMISSION_GRANTED -> "authorized"
else -> "not-determined"
@ -127,8 +127,11 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
// Filters out cameras that are LEGACY hardware level. Those don't support Preview + Photo Capture + Video Capture at the same time.
if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
Log.i(REACT_CLASS, "Skipping Camera #${id} because it does not meet the minimum requirements for react-native-vision-camera. " +
"See the tables at https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture for more information.")
Log.i(
REACT_CLASS,
"Skipping Camera #$id because it does not meet the minimum requirements for react-native-vision-camera. " +
"See the tables at https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture for more information."
)
return@loop
}

View File

@ -25,37 +25,42 @@ abstract class CameraError(
* A throwable that caused this error.
*/
cause: Throwable? = null
): Throwable("[$domain/$id] $message", cause)
) : Throwable("[$domain/$id] $message", cause)
val CameraError.code: String
get() = "$domain/$id"
class MicrophonePermissionError : CameraError("permission", "microphone-permission-denied", "The Microphone permission was denied!")
class CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
class MicrophonePermissionError: CameraError("permission", "microphone-permission-denied", "The Microphone permission was denied!")
class CameraPermissionError: CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
class InvalidTypeScriptUnionError(unionName: String, unionValue: String) : CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)")
class UnsupportedOSError(unionName: String, unionValue: String, supportedOnOS: String) : CameraError("parameter", "unsupported-os", "The given value \"$unionValue\" could not be used for $unionName, as it is only available on Android $supportedOnOS and above!")
class InvalidTypeScriptUnionError(unionName: String, unionValue: String): CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)")
class UnsupportedOSError(unionName: String, unionValue: String, supportedOnOS: String): CameraError("parameter", "unsupported-os", "The given value \"$unionValue\" could not be used for $unionName, as it is only available on Android $supportedOnOS and above!")
class NoCameraDeviceError : CameraError("device", "no-device", "No device was set! Use `getAvailableCameraDevices()` to select a suitable Camera device.")
class InvalidCameraDeviceError(cause: Throwable) : CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause)
class NoCameraDeviceError: CameraError("device", "no-device", "No device was set! Use `getAvailableCameraDevices()` to select a suitable Camera device.")
class InvalidCameraDeviceError(cause: Throwable): CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause)
class FpsNotContainedInFormatError(fps: Int) : CameraError("format", "invalid-fps", "The given FPS were not valid for the currently selected format. Make sure you select a format which `frameRateRanges` includes $fps FPS!")
class HdrNotContainedInFormatError() : CameraError(
"format", "invalid-hdr",
"The currently selected format does not support HDR capture! " +
"Make sure you select a format which `frameRateRanges` includes `supportsPhotoHDR`!"
)
class LowLightBoostNotContainedInFormatError() : CameraError(
"format", "invalid-low-light-boost",
"The currently selected format does not support low-light boost (night mode)! " +
"Make sure you select a format which includes `supportsLowLightBoost`."
)
class FpsNotContainedInFormatError(fps: Int): CameraError("format", "invalid-fps", "The given FPS were not valid for the currently selected format. Make sure you select a format which `frameRateRanges` includes $fps FPS!")
class HdrNotContainedInFormatError(): CameraError("format", "invalid-hdr", "The currently selected format does not support HDR capture! " +
"Make sure you select a format which `frameRateRanges` includes `supportsPhotoHDR`!")
class LowLightBoostNotContainedInFormatError(): CameraError("format", "invalid-low-light-boost", "The currently selected format does not support low-light boost (night mode)! " +
"Make sure you select a format which includes `supportsLowLightBoost`.")
class CameraNotReadyError : CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
class CameraNotReadyError: CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
class InvalidFormatError(format: Int) : CameraError("capture", "invalid-photo-format", "The Photo has an invalid format! Expected ${ImageFormat.YUV_420_888}, actual: $format")
class VideoEncoderError(message: String, cause: Throwable? = null) : CameraError("capture", "encoder-error", message, cause)
class VideoMuxerError(message: String, cause: Throwable? = null) : CameraError("capture", "muxer-error", message, cause)
class RecordingInProgressError(message: String, cause: Throwable? = null) : CameraError("capture", "recording-in-progress", message, cause)
class FileIOError(message: String, cause: Throwable? = null) : CameraError("capture", "file-io-error", message, cause)
class InvalidCameraError(message: String, cause: Throwable? = null) : CameraError("capture", "not-bound-error", message, cause)
class InvalidFormatError(format: Int): CameraError("capture", "invalid-photo-format", "The Photo has an invalid format! Expected ${ImageFormat.YUV_420_888}, actual: $format")
class VideoEncoderError(message: String, cause: Throwable? = null): CameraError("capture", "encoder-error", message, cause)
class VideoMuxerError(message: String, cause: Throwable? = null): CameraError("capture", "muxer-error", message, cause)
class RecordingInProgressError(message: String, cause: Throwable? = null): CameraError("capture", "recording-in-progress", message, cause)
class FileIOError(message: String, cause: Throwable? = null): CameraError("capture", "file-io-error", message, cause)
class InvalidCameraError(message: String, cause: Throwable? = null): CameraError("capture", "not-bound-error", message, cause)
class CameraManagerUnavailableError : CameraError("system", "no-camera-manager", "The Camera manager instance was unavailable for the current Application!")
class ViewNotFoundError(viewId: Int) : CameraError("system", "view-not-found", "The given view (ID $viewId) was not found in the view manager.")
class CameraManagerUnavailableError: CameraError("system", "no-camera-manager", "The Camera manager instance was unavailable for the current Application!")
class ViewNotFoundError(viewId: Int): CameraError("system", "view-not-found", "The given view (ID $viewId) was not found in the view manager.")
class UnknownCameraError(cause: Throwable): CameraError("unknown", "unknown", cause.message ?: "An unknown camera error occured.", cause)
class UnknownCameraError(cause: Throwable) : CameraError("unknown", "unknown", cause.message ?: "An unknown camera error occured.", cause)

View File

@ -2,14 +2,12 @@ package com.mrousavy.camera.utils
import android.hardware.camera2.CameraCharacteristics
import android.util.Size
import com.mrousavy.camera.parsers.bigger
import com.mrousavy.camera.parsers.parseLensFacing
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.mrousavy.camera.parsers.bigger
import kotlin.math.PI
import kotlin.math.atan
// 35mm is 135 film format, a standard in which focal lengths are usually measured
val Size35mm = Size(36, 24)
@ -38,7 +36,7 @@ fun CameraCharacteristics.getDeviceTypes(): ReadableArray {
val deviceTypes = Arguments.createArray()
val containsTelephoto = focalLengths.any { l -> (l * cropFactor) > 35 } // TODO: Telephoto lenses are > 85mm, but we don't have anything between that range..
//val containsNormalLens = focalLengths.any { l -> (l * cropFactor) > 35 && (l * cropFactor) <= 55 }
// val containsNormalLens = focalLengths.any { l -> (l * cropFactor) > 35 && (l * cropFactor) <= 55 }
val containsWideAngle = focalLengths.any { l -> (l * cropFactor) >= 24 && (l * cropFactor) <= 35 }
val containsUltraWideAngle = focalLengths.any { l -> (l * cropFactor) < 24 }

View File

@ -5,7 +5,6 @@ import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.CameraSelector
import java.lang.IllegalArgumentException
/**
* Create a new [CameraSelector] which selects the camera with the given [cameraId]
*/

View File

@ -24,7 +24,7 @@ class DeviceFormat(map: ReadableMap) {
}
fun rangeFactory(minFrameRate: Any?, maxFrameRate: Any?): Range<Int> {
return when(minFrameRate) {
return when (minFrameRate) {
is Int -> Range(minFrameRate, maxFrameRate as Int)
is Double -> Range(minFrameRate.toInt(), (maxFrameRate as Double).toInt())
else -> throw IllegalArgumentException()

View File

@ -4,7 +4,6 @@ import androidx.exifinterface.media.ExifInterface
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
fun ExifInterface.buildMetadataMap(): WritableMap {
val metadataMap = Arguments.createMap()
metadataMap.putInt("Orientation", this.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL))

View File

@ -4,14 +4,14 @@ import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
suspend inline fun ImageCapture.takePicture(options: ImageCapture.OutputFileOptions, executor: Executor) = suspendCoroutine<ImageCapture.OutputFileResults> { cont ->
this.takePicture(options, executor, object: ImageCapture.OnImageSavedCallback {
this.takePicture(
options, executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
cont.resume(outputFileResults)
}
@ -19,11 +19,14 @@ suspend inline fun ImageCapture.takePicture(options: ImageCapture.OutputFileOpti
override fun onError(exception: ImageCaptureException) {
cont.resumeWithException(exception)
}
})
}
)
}
suspend inline fun ImageCapture.takePicture(executor: Executor) = suspendCoroutine<ImageProxy> { cont ->
this.takePicture(executor, object: ImageCapture.OnImageCapturedCallback() {
this.takePicture(
executor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
cont.resume(image)
@ -33,5 +36,6 @@ suspend inline fun ImageCapture.takePicture(executor: Executor) = suspendCorouti
super.onError(exception)
cont.resumeWithException(exception)
}
})
}
)
}

View File

@ -1,18 +1,16 @@
package com.mrousavy.camera.utils
import android.annotation.SuppressLint
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import androidx.camera.core.ImageProxy
import com.mrousavy.camera.InvalidFormatError
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.util.stream.Stream.concat
// TODO: Fix this flip() function (this outputs a black image)
fun flip(imageBytes: ByteArray, imageWidth: Int): ByteArray {
//separate out the sub arrays
// separate out the sub arrays
var holder = ByteArray(imageBytes.size)
var subArray = ByteArray(imageWidth)
var subCount = 0
@ -35,7 +33,6 @@ fun flip(imageBytes: ByteArray, imageWidth: Int): ByteArray {
return holder + subArray
}
@SuppressLint("UnsafeExperimentalUsageError")
fun ImageProxy.save(file: File, flipHorizontally: Boolean) {
when (format) {

View File

@ -9,7 +9,10 @@ import kotlin.coroutines.suspendCoroutine
suspend fun getCameraProvider(context: Context) = suspendCoroutine<ProcessCameraProvider> { cont ->
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProviderFuture.addListener(
{
cont.resume(cameraProviderFuture.get())
}, ContextCompat.getMainExecutor(context))
},
ContextCompat.getMainExecutor(context)
)
}

View File

@ -3,7 +3,6 @@ package com.mrousavy.camera.utils
import android.util.Size
import android.view.Surface
/**
* Rotate by a given Surface Rotation
*/

View File

@ -1,8 +1,8 @@
package com.mrousavy.camera.utils
import com.facebook.react.bridge.Promise
import com.mrousavy.camera.CameraError
import com.mrousavy.camera.UnknownCameraError
import com.facebook.react.bridge.Promise
inline fun withPromise(promise: Promise, closure: () -> Any?) {
try {