From d68eb4f4107576504887969eda53256b4d1c1e5c Mon Sep 17 00:00:00 2001 From: Robert Jaros Date: Sat, 25 May 2019 13:49:14 +0200 Subject: Implement Cordova API: lifecycle events, camera plugin. --- .../kotlin/pl/treksoft/kvision/cordova/Battery.kt | 6 +- .../kotlin/pl/treksoft/kvision/cordova/Camera.kt | 269 +++++++++++++++++++++ .../kotlin/pl/treksoft/kvision/cordova/Device.kt | 52 +++- .../kotlin/pl/treksoft/kvision/cordova/Result.kt | 151 ++++++++++++ 4 files changed, 467 insertions(+), 11 deletions(-) create mode 100644 kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Camera.kt create mode 100644 kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Result.kt (limited to 'kvision-modules/kvision-cordova/src/main/kotlin') diff --git a/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Battery.kt b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Battery.kt index c8c26475..c6ac2a00 100644 --- a/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Battery.kt +++ b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Battery.kt @@ -27,7 +27,7 @@ import kotlin.browser.window /** * Battery status event types. */ -enum class BatteryEvent(internal var event: String) { +enum class BatteryEvent(internal val event: String) { BATTERY_STATUS("batterystatus"), BATTERY_LOW("batterylow"), BATTERY_CRITICAL("batterycritical") @@ -37,8 +37,8 @@ enum class BatteryEvent(internal var event: String) { * Battery status. */ external class BatteryStatus { - val level: Int = definedExternally - val isPlugged: Boolean = definedExternally + val level: Int + val isPlugged: Boolean } /** diff --git a/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Camera.kt b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Camera.kt new file mode 100644 index 00000000..885bfd08 --- /dev/null +++ b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Camera.kt @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package pl.treksoft.kvision.cordova + +import pl.treksoft.kvision.utils.obj +import kotlin.browser.window +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * Exception class for camera errors. + */ +class CameraException(message: String) : Exception(message) + +/** + * Main object for Cordova camera. + */ +object Camera { + + private const val CAMERA_ACTIVE_STORAGE_KEY = "kv_camera_active_storage_key" + private const val CAMERA_STATUS_OK = "OK" + + /** + * Camera destination types. + */ + enum class DestinationType { + DATA_URL, + FILE_URI, + NATIVE_URI + } + + /** + * Picture encoding types. + */ + enum class EncodingType { + JPEG, + PNG + } + + /** + * Picture/video media types. + */ + enum class MediaType { + PICTURE, + VIDEO, + ALLMEDIA + } + + /** + * Camera picture/video sources. + */ + enum class PictureSourceType { + PHOTOLIBRARY, + CAMERA, + SAVEDPHOTOALBUM + } + + /** + * Camera facing types. + */ + enum class Direction { + BACK, + FRONT + } + + /** + * iOS popover arrow directions. + */ + enum class PopoverArrowDirection { + ARROW_UP, + ARROW_DOWN, + ARROW_LEFT, + ARROW_RIGHT, + ARROW_ANY + } + + /** + * iOS popover options. + */ + data class CameraPopoverOptions( + val x: Int, + val y: Int, + val width: Int, + val height: Int, + val arrowDir: PopoverArrowDirection, + val popoverWidth: Int, + val popoverHeight: Int + ) + + /** + * Suspending function to get picture from the camera. + * + * Note: On Android platform you must also use [addCameraResultCallback] listener. + * + * @param options camera options + * @return a [Result] class containing the picture or the exception + */ + @Suppress("UnsafeCastFromDynamic") + suspend fun getPicture(options: CameraOptions): Result { + return suspendCoroutine { continuation -> + getPicture(options) { + continuation.resume(it) + } + } + } + + /** + * A function to get picture from the camera. + * + * Note: On Android platform you must also use [addCameraResultCallback] listener. + * + * @param options camera options + * @param resultCallback a callback function to get the [Result], containing the picture or the exception + */ + @Suppress("UnsafeCastFromDynamic") + fun getPicture(options: CameraOptions, resultCallback: (Result) -> Unit) { + window.localStorage.setItem(CAMERA_ACTIVE_STORAGE_KEY, "true") + addDeviceReadyListener { + window.navigator.asDynamic().camera.getPicture({ image -> + window.localStorage.removeItem(CAMERA_ACTIVE_STORAGE_KEY) + resultCallback(Result.success(image)) + }, { message -> + window.localStorage.removeItem(CAMERA_ACTIVE_STORAGE_KEY) + resultCallback(Result.error(CameraException(message))) + }, options.toJs()) + } + } + + /** + * An Android specific function to get picture from the camera after resume when the application + * webview intent is killed. + * + * @param resultCallback a callback function to get the [Result], containing the picture or the exception + */ + fun addCameraResultCallback(resultCallback: (Result) -> Unit) { + addResumeListener { resumeEvent -> + val isCameraActive = window.localStorage.getItem(CAMERA_ACTIVE_STORAGE_KEY) == "true" + if (isCameraActive && resumeEvent.pendingResult != null) { + window.localStorage.removeItem(CAMERA_ACTIVE_STORAGE_KEY) + if (resumeEvent.pendingResult.pluginStatus == CAMERA_STATUS_OK) { + resultCallback(Result.success(resumeEvent.pendingResult.result)) + } else { + resultCallback(Result.error(CameraException(resumeEvent.pendingResult.result))) + } + } + } + } + + /** + * Removes intermediate image files that are kept in the temporary storage after calling [getPicture]. + * + * @param resultCallback an optional callback function to get the [Result] of the cleanup operation. + */ + @Suppress("UnsafeCastFromDynamic") + fun cleanup(resultCallback: ((Result) -> Unit)? = null) { + addDeviceReadyListener { + window.navigator.asDynamic().camera.cleanup({ + resultCallback?.invoke(Result.success(CAMERA_STATUS_OK)) + }, { message -> + resultCallback?.invoke(Result.error(CameraException(message))) + }) + } + } +} + +internal fun Camera.DestinationType.toJs(): dynamic = when (this) { + Camera.DestinationType.DATA_URL -> js("Camera.DestinationType.DATA_URL") + Camera.DestinationType.FILE_URI -> js("Camera.DestinationType.FILE_URI") + Camera.DestinationType.NATIVE_URI -> js("Camera.DestinationType.NATIVE_URI") +} + +internal fun Camera.EncodingType.toJs(): dynamic = when (this) { + Camera.EncodingType.JPEG -> js("Camera.EncodingType.JPEG") + Camera.EncodingType.PNG -> js("Camera.EncodingType.PNG") +} + +internal fun Camera.MediaType.toJs(): dynamic = when (this) { + Camera.MediaType.PICTURE -> js("Camera.MediaType.PICTURE") + Camera.MediaType.VIDEO -> js("Camera.MediaType.VIDEO") + Camera.MediaType.ALLMEDIA -> js("Camera.MediaType.ALLMEDIA") +} + +internal fun Camera.PictureSourceType.toJs(): dynamic = when (this) { + Camera.PictureSourceType.PHOTOLIBRARY -> js("Camera.PictureSourceType.PHOTOLIBRARY") + Camera.PictureSourceType.CAMERA -> js("Camera.PictureSourceType.CAMERA") + Camera.PictureSourceType.SAVEDPHOTOALBUM -> js("Camera.PictureSourceType.SAVEDPHOTOALBUM") +} + +internal fun Camera.PopoverArrowDirection.toJs(): dynamic = when (this) { + Camera.PopoverArrowDirection.ARROW_UP -> js("Camera.PopoverArrowDirection.ARROW_UP") + Camera.PopoverArrowDirection.ARROW_DOWN -> js("Camera.PopoverArrowDirection.ARROW_DOWN") + Camera.PopoverArrowDirection.ARROW_LEFT -> js("Camera.PopoverArrowDirection.ARROW_LEFT") + Camera.PopoverArrowDirection.ARROW_RIGHT -> js("Camera.PopoverArrowDirection.ARROW_RIGHT") + Camera.PopoverArrowDirection.ARROW_ANY -> js("Camera.PopoverArrowDirection.ARROW_ANY") +} + +internal fun Camera.Direction.toJs(): dynamic = when (this) { + Camera.Direction.BACK -> js("Camera.Direction.BACK") + Camera.Direction.FRONT -> js("Camera.Direction.FRONT") +} + +internal external class CameraPopoverOptions( + x: Int, + y: Int, + width: Int, + height: Int, + arrowDir: dynamic, + popoverWidth: Int, + popoverHeight: Int +) + +internal fun Camera.CameraPopoverOptions.toJs(): dynamic { + return CameraPopoverOptions(x, y, width, height, arrowDir.toJs(), popoverWidth, popoverHeight) +} + +/** + * Camera options. + */ +data class CameraOptions( + val quality: Int? = null, + val destinationType: Camera.DestinationType? = null, + val sourceType: Camera.PictureSourceType? = null, + val allowEdit: Boolean? = null, + val encodingType: Camera.EncodingType? = null, + val targetWidth: Int? = null, + val targetHeight: Int? = null, + val mediaType: Camera.MediaType? = null, + val correctOrientation: Boolean? = null, + val saveToPhotoAlbum: Boolean? = null, + val popoverOptions: Camera.CameraPopoverOptions? = null, + val cameraDirection: Camera.Direction? = null +) + +internal fun CameraOptions.toJs(): dynamic { + return obj { + if (quality != null) this.quality = quality + if (destinationType != null) this.destinationType = destinationType.toJs() + if (sourceType != null) this.sourceType = sourceType.toJs() + if (allowEdit != null) this.allowEdit = allowEdit + if (encodingType != null) this.encodingType = encodingType.toJs() + if (targetWidth != null) this.targetWidth = targetWidth + if (targetHeight != null) this.targetHeight = targetHeight + if (mediaType != null) this.mediaType = mediaType.toJs() + if (correctOrientation != null) this.correctOrientation = correctOrientation + if (saveToPhotoAlbum != null) this.saveToPhotoAlbum = saveToPhotoAlbum + if (popoverOptions != null) this.popoverOptions = popoverOptions.toJs() + if (cameraDirection != null) this.cameraDirection = cameraDirection.toJs() + } +} diff --git a/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Device.kt b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Device.kt index bf273ca7..69e3defb 100644 --- a/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Device.kt +++ b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Device.kt @@ -30,14 +30,31 @@ import kotlin.coroutines.suspendCoroutine * Device information class. */ external class Device { - val cordova: String = definedExternally - val model: String = definedExternally - val platform: String = definedExternally - val uuid: String = definedExternally - val version: String = definedExternally - val manufacturer: String = definedExternally - val isVirtual: Boolean = definedExternally - val serial: String = definedExternally + val cordova: String + val model: String + val platform: String + val uuid: String + val version: String + val manufacturer: String + val isVirtual: Boolean + val serial: String +} + +/** + * Pending result class. + */ +external class PendingResult { + val pluginServiceName: String + val pluginStatus: String + val result: dynamic +} + +/** + * Resume event class. + */ +external class ResumeEvent { + val action: String? + val pendingResult: PendingResult? } /** @@ -56,6 +73,25 @@ fun addDeviceReadyListener(listener: (Device) -> Unit) { }, false) } +/** + * Add listeners for 'pause' Cordova event. + */ +fun addPauseListener(listener: () -> Unit) { + document.addEventListener("pause", { + listener() + }, false) +} + +/** + * Add listeners for 'resume' Cordova event. + */ +fun addResumeListener(listener: (ResumeEvent) -> Unit) { + document.addEventListener("resume", { e -> + @Suppress("UnsafeCastFromDynamic") + listener(e.asDynamic()) + }, false) +} + /** * Suspending function to return device information object. */ diff --git a/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Result.kt b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Result.kt new file mode 100644 index 00000000..bb125e24 --- /dev/null +++ b/kvision-modules/kvision-cordova/src/main/kotlin/pl/treksoft/kvision/cordova/Result.kt @@ -0,0 +1,151 @@ +/* + * Source: https://github.com/kittinunf/Result + * + * MIT License + * + * Copyright (c) 2017 Kittinun Vantasin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package pl.treksoft.kvision.cordova + +inline fun Result<*, *>.getAs() = when (this) { + is Result.Success -> value as? X + is Result.Failure -> error as? X +} + +fun Result.success(f: (V) -> Unit) = fold(f, {}) + +fun Result<*, E>.failure(f: (E) -> Unit) = fold({}, f) + +infix fun Result.or(fallback: V) = when (this) { + is Result.Success -> this + else -> Result.Success(fallback) +} + +infix fun Result.getOrElse(fallback: V) = when (this) { + is Result.Success -> value + else -> fallback +} + +inline fun Result.map(transform: (V) -> U): Result = try { + when (this) { + is Result.Success -> Result.Success(transform(value)) + is Result.Failure -> Result.Failure(error) + } +} catch (ex: Exception) { + @Suppress("UNCHECKED_CAST") + Result.error(ex as E) +} + +inline fun Result.flatMap(transform: (V) -> Result): Result = try { + when (this) { + is Result.Success -> transform(value) + is Result.Failure -> Result.Failure(error) + } +} catch (ex: Exception) { + @Suppress("UNCHECKED_CAST") + Result.error(ex as E) +} + +fun Result.mapError(transform: (E) -> E2) = when (this) { + is Result.Success -> Result.Success(value) + is Result.Failure -> Result.Failure(transform(error)) +} + +fun Result.flatMapError(transform: (E) -> Result) = when (this) { + is Result.Success -> Result.Success(value) + is Result.Failure -> transform(error) +} + +fun Result.any(predicate: (V) -> Boolean): Boolean = try { + when (this) { + is Result.Success -> predicate(value) + is Result.Failure -> false + } +} catch (ex: Exception) { + false +} + +fun Result.fanout(other: () -> Result): Result, *> = + flatMap { outer -> other().map { outer to it } } + +sealed class Result { + + open operator fun component1(): V? = null + open operator fun component2(): E? = null + + inline fun fold(success: (V) -> X, failure: (E) -> X): X = when (this) { + is Success -> success(this.value) + is Failure -> failure(this.error) + } + + abstract fun get(): V + + class Success(val value: V) : Result() { + override fun component1(): V? = value + + override fun get(): V = value + + override fun toString() = "[Success: $value]" + + override fun hashCode(): Int = value.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Success<*> && value == other.value + } + } + + class Failure(val error: E) : Result() { + override fun component2(): E? = error + + override fun get() = throw error + + fun getException(): E = error + + override fun toString() = "[Failure: $error]" + + override fun hashCode(): Int = error.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Failure<*> && error == other.error + } + } + + companion object { + // Factory methods + fun error(ex: E) = Failure(ex) + + fun success(v: V) = Success(v) + + fun of(value: V?, fail: (() -> Exception) = { Exception() }): Result = + value?.let { success(it) } ?: error(fail()) + + fun of(f: () -> V): Result = try { + success(f()) + } catch (ex: Exception) { + @Suppress("UNCHECKED_CAST") + error(ex as E) + } + } + +} -- cgit