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: paths:
- '.github/workflows/validate-android.yml' - '.github/workflows/validate-android.yml'
- 'android/**' - 'android/**'
- '.editorconfig'
pull_request: pull_request:
paths: paths:
- '.github/workflows/validate-android.yml' - '.github/workflows/validate-android.yml'
- 'android/**' - 'android/**'
- '.editorconfig'
jobs: jobs:
lint: lint:
@ -32,3 +34,12 @@ jobs:
- uses: yutailang0119/action-android-lint@v1.0.2 - uses: yutailang0119/action-android-lint@v1.0.2
with: with:
xml_path: android/build/reports/lint-results.xml 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 // noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$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: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
def getExtOrDefault(name) { def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['VisionCamera_' + 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 android.annotation.SuppressLint
import androidx.camera.core.VideoCapture import androidx.camera.core.VideoCapture
import com.mrousavy.camera.utils.makeErrorMap
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.mrousavy.camera.utils.makeErrorMap
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
@ -26,7 +26,9 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
} }
val videoFileOptions = VideoCapture.OutputFileOptions.Builder(videoFile) 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) { override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
val map = Arguments.createMap() val map = Arguments.createMap()
map.putString("path", videoFile.absolutePath) map.putString("path", videoFile.absolutePath)
@ -52,12 +54,12 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
// reset the torch mode // reset the torch mode
camera!!.cameraControl.enableTorch(torch == "on") camera!!.cameraControl.enableTorch(torch == "on")
} }
}) }
)
return TemporaryFile(videoFile.absolutePath) return TemporaryFile(videoFile.absolutePath)
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
fun CameraView.stopRecording() { fun CameraView.stopRecording() {
if (videoCapture == null) { if (videoCapture == null) {

View File

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

View File

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

View File

@ -20,9 +20,9 @@ import androidx.camera.extensions.NightPreviewExtender
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.* import androidx.lifecycle.*
import com.mrousavy.camera.utils.*
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.facebook.react.uimanager.events.RCTEventEmitter import com.facebook.react.uimanager.events.RCTEventEmitter
import com.mrousavy.camera.utils.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.util.concurrent.Executors import java.util.concurrent.Executors

View File

@ -1,14 +1,12 @@
package com.mrousavy.camera package com.mrousavy.camera
import android.util.Log import android.util.Log
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.annotations.ReactProp
import java.lang.ref.WeakReference
class CameraViewManager : SimpleViewManager<CameraView>() { class CameraViewManager : SimpleViewManager<CameraView>() {
private fun addChangedPropToTransaction(view: CameraView, changedProp: String) { 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.HdrImageCaptureExtender
import androidx.camera.extensions.NightImageCaptureExtender import androidx.camera.extensions.NightImageCaptureExtender
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.mrousavy.camera.parsers.*
import com.mrousavy.camera.utils.*
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener import com.facebook.react.modules.core.PermissionListener
import com.mrousavy.camera.parsers.*
import com.mrousavy.camera.utils.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -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. // 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) { 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. " + Log.i(
"See the tables at https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture for more information.") 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 return@loop
} }

View File

@ -30,7 +30,6 @@ abstract class CameraError(
val CameraError.code: String val CameraError.code: String
get() = "$domain/$id" get() = "$domain/$id"
class MicrophonePermissionError : CameraError("permission", "microphone-permission-denied", "The Microphone 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 CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
@ -41,10 +40,16 @@ class NoCameraDeviceError: CameraError("device", "no-device", "No device was set
class InvalidCameraDeviceError(cause: Throwable) : CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause) 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 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! " + class HdrNotContainedInFormatError() : CameraError(
"Make sure you select a format which `frameRateRanges` includes `supportsPhotoHDR`!") "format", "invalid-hdr",
class LowLightBoostNotContainedInFormatError(): CameraError("format", "invalid-low-light-boost", "The currently selected format does not support low-light boost (night mode)! " + "The currently selected format does not support HDR capture! " +
"Make sure you select a format which includes `supportsLowLightBoost`.") "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!")

View File

@ -2,14 +2,12 @@ package com.mrousavy.camera.utils
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.util.Size 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.Arguments
import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableArray
import com.mrousavy.camera.parsers.bigger
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.atan import kotlin.math.atan
// 35mm is 135 film format, a standard in which focal lengths are usually measured // 35mm is 135 film format, a standard in which focal lengths are usually measured
val Size35mm = Size(36, 24) val Size35mm = Size(36, 24)

View File

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

View File

@ -4,7 +4,6 @@ import androidx.exifinterface.media.ExifInterface
import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableMap
fun ExifInterface.buildMetadataMap(): WritableMap { fun ExifInterface.buildMetadataMap(): WritableMap {
val metadataMap = Arguments.createMap() val metadataMap = Arguments.createMap()
metadataMap.putInt("Orientation", this.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) 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.ImageCaptureException
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
suspend inline fun ImageCapture.takePicture(options: ImageCapture.OutputFileOptions, executor: Executor) = suspendCoroutine<ImageCapture.OutputFileResults> { cont -> 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) { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
cont.resume(outputFileResults) cont.resume(outputFileResults)
} }
@ -19,11 +19,14 @@ suspend inline fun ImageCapture.takePicture(options: ImageCapture.OutputFileOpti
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
cont.resumeWithException(exception) cont.resumeWithException(exception)
} }
}) }
)
} }
suspend inline fun ImageCapture.takePicture(executor: Executor) = suspendCoroutine<ImageProxy> { cont -> 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) { override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image) super.onCaptureSuccess(image)
cont.resume(image) cont.resume(image)
@ -33,5 +36,6 @@ suspend inline fun ImageCapture.takePicture(executor: Executor) = suspendCorouti
super.onError(exception) super.onError(exception)
cont.resumeWithException(exception) cont.resumeWithException(exception)
} }
}) }
)
} }

View File

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

View File

@ -9,7 +9,10 @@ import kotlin.coroutines.suspendCoroutine
suspend fun getCameraProvider(context: Context) = suspendCoroutine<ProcessCameraProvider> { cont -> suspend fun getCameraProvider(context: Context) = suspendCoroutine<ProcessCameraProvider> { cont ->
val cameraProviderFuture = ProcessCameraProvider.getInstance(context) val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({ cameraProviderFuture.addListener(
{
cont.resume(cameraProviderFuture.get()) 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.util.Size
import android.view.Surface import android.view.Surface
/** /**
* Rotate by a given Surface Rotation * Rotate by a given Surface Rotation
*/ */

View File

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