This commit is contained in:
metacryst
2026-01-09 11:14:27 -06:00
parent cf03c95664
commit 637c9e4674
2149 changed files with 527743 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
ext {
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
playServicesLocationVersion = project.hasProperty('playServicesLocationVersion') ? rootProject.ext.playServicesLocationVersion : '21.3.0'
}
buildscript {
ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '1.9.25'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "com.capacitorjs.plugins.geolocation"
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
defaultConfig {
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
kotlin {
jvmToolchain(21)
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation("io.ionic.libs:iongeolocation-android:1.0.0")
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation 'com.google.code.gson:gson:2.10.1'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4")
implementation "com.google.android.gms:play-services-location:$playServicesLocationVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
}

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,71 @@
package com.capacitorjs.plugins.geolocation
/**
* Object with plugin errors
*/
object GeolocationErrors {
private fun formatErrorCode(number: Int): String {
return "OS-PLUG-GLOC-" + number.toString().padStart(4, '0')
}
data class ErrorInfo(
val code: String,
val message: String
)
val POSITION_UNAVAILABLE = ErrorInfo(
code = formatErrorCode(2),
message = "There was en error trying to obtain the location."
)
val LOCATION_PERMISSIONS_DENIED = ErrorInfo(
code = formatErrorCode(3),
message = "Location permission request was denied."
)
val LOCATION_DISABLED = ErrorInfo(
code = formatErrorCode(7),
message = "Location services are not enabled."
)
val LOCATION_ENABLE_REQUEST_DENIED = ErrorInfo(
code = formatErrorCode(9),
message = "Request to enable location was denied."
)
val GET_LOCATION_TIMEOUT = ErrorInfo(
code = formatErrorCode(10),
message = "Could not obtain location in time. Try with a higher timeout."
)
val INVALID_TIMEOUT = ErrorInfo(
code = formatErrorCode(11),
message = "Timeout needs to be a positive value."
)
val WATCH_ID_NOT_FOUND = ErrorInfo(
code = formatErrorCode(12),
message = "WatchId not found."
)
val WATCH_ID_NOT_PROVIDED = ErrorInfo(
code = formatErrorCode(13),
message = "WatchId needs to be provided."
)
val GOOGLE_SERVICES_RESOLVABLE = ErrorInfo(
code = formatErrorCode(14),
message = "Google Play Services error user resolvable."
)
val GOOGLE_SERVICES_ERROR = ErrorInfo(
code = formatErrorCode(15),
message = "Google Play Services error."
)
val LOCATION_SETTINGS_ERROR = ErrorInfo(
code = formatErrorCode(16),
message = "Location settings error."
)
}

View File

@@ -0,0 +1,341 @@
package com.capacitorjs.plugins.geolocation
import android.Manifest
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import com.getcapacitor.JSObject
import com.getcapacitor.PermissionState
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import com.getcapacitor.annotation.Permission
import com.getcapacitor.annotation.PermissionCallback
import com.google.android.gms.location.LocationServices
import io.ionic.libs.iongeolocationlib.controller.IONGLOCController
import io.ionic.libs.iongeolocationlib.model.IONGLOCException
import io.ionic.libs.iongeolocationlib.model.IONGLOCLocationOptions
import io.ionic.libs.iongeolocationlib.model.IONGLOCLocationResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
@CapacitorPlugin(
name = "Geolocation",
permissions = [Permission(
strings = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION],
alias = GeolocationPlugin.LOCATION_ALIAS
), Permission(
strings = [Manifest.permission.ACCESS_COARSE_LOCATION],
alias = GeolocationPlugin.COARSE_LOCATION_ALIAS
)]
)
class GeolocationPlugin : Plugin() {
private lateinit var controller: IONGLOCController
private lateinit var coroutineScope: CoroutineScope
private val watchingCalls: MutableMap<String, PluginCall> = mutableMapOf()
companion object {
const val LOCATION_ALIAS: String = "location"
const val COARSE_LOCATION_ALIAS: String = "coarseLocation"
}
override fun load() {
super.load()
coroutineScope = CoroutineScope(Dispatchers.Main)
val activityLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
CoroutineScope(Dispatchers.Main).launch {
controller.onResolvableExceptionResult(result.resultCode)
}
}
this.controller = IONGLOCController(
LocationServices.getFusedLocationProviderClient(context),
activityLauncher
)
}
override fun handleOnDestroy() {
super.handleOnDestroy()
coroutineScope.cancel()
}
@PluginMethod
override fun checkPermissions(call: PluginCall) {
checkLocationState(call) { super.checkPermissions(call) }
}
@PluginMethod
override fun requestPermissions(call: PluginCall) {
checkLocationState(call) { super.requestPermissions(call) }
}
/**
* Helper function to determine if location services are enabled or not
* @param call the PluginCall to use in case we want to send an error
* @param onLocationEnabled lambda function to use in case location services are enabled
*/
private fun checkLocationState(call: PluginCall, onLocationEnabled: () -> Unit) {
if (controller.areLocationServicesEnabled(context)) {
onLocationEnabled()
} else {
call.sendError(GeolocationErrors.LOCATION_DISABLED)
}
}
/**
* Checks location permission state, requesting them if necessary.
* If not, calls getPosition to get the device's position
* @param call the plugin call
*/
@PluginMethod
fun getCurrentPosition(call: PluginCall) {
handlePermissionRequest(call, "completeCurrentPosition") { getPosition(call) }
}
/**
* Checks location permission state, requesting them if necessary.
* If not, calls startWatch to start getting location updates
* @param call the plugin call
*/
@PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
fun watchPosition(call: PluginCall) {
handlePermissionRequest(call, "completeWatchPosition") { startWatch(call) }
}
/**
* Helper function to determine if a permission is granted or not and request it if necessary
* @param call the PluginCall to use in case we want to send an error
* @param callbackName a string identifying the callback to call once the permission prompt is answered
* @param onPermissionGranted lambda function to use in case the permission is enabled
*/
private fun handlePermissionRequest(
call: PluginCall,
callbackName: String,
onPermissionGranted: () -> Unit
) {
val alias = getAlias(call)
if (getPermissionState(alias) != PermissionState.GRANTED) {
requestPermissionForAlias(alias, call, callbackName)
} else {
onPermissionGranted()
}
}
/**
* Completes the getCurrentPosition plugin call after a permission request
* @see .getCurrentPosition
* @param call the plugin call
*/
@PermissionCallback
private fun completeCurrentPosition(call: PluginCall) {
handlePermissionResult(call) { getPosition(call) }
}
/**
* Completes the watchPosition plugin call after a permission request
* @see .startWatch
* @param call the plugin call
*/
@PermissionCallback
private fun completeWatchPosition(call: PluginCall) {
handlePermissionResult(call) { startWatch(call) }
}
/**
* Helper function to handle the result of a location permission request
* @param call the PluginCall to use in case we want to send an error
* @param onPermissionGranted lambda function to use in case the permission was granted
*/
private fun handlePermissionResult(call: PluginCall, onPermissionGranted: () -> Unit) {
if (getPermissionState(COARSE_LOCATION_ALIAS) == PermissionState.GRANTED) {
onPermissionGranted()
} else {
call.sendError(GeolocationErrors.LOCATION_PERMISSIONS_DENIED)
}
}
/**
* Clears the watch, removing location updates.
* @param call the plugin call
*/
@PluginMethod
fun clearWatch(call: PluginCall) {
val id = call.getString("id")
if (id.isNullOrBlank()) {
call.sendError(GeolocationErrors.WATCH_ID_NOT_PROVIDED)
} else {
watchingCalls.remove(id)?.release(bridge)
val watchCleared = controller.clearWatch(id)
if (watchCleared) {
call.sendSuccess()
} else {
call.sendError(GeolocationErrors.WATCH_ID_NOT_FOUND)
}
}
}
/**
* Gets the appropriate permission alias
* @param call the plugin call
* @return String with correct alias
*/
private fun getAlias(call: PluginCall): String {
var alias = LOCATION_ALIAS
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val enableHighAccuracy = call.getBoolean("enableHighAccuracy") ?: false
if (!enableHighAccuracy) {
alias = COARSE_LOCATION_ALIAS
}
}
return alias
}
/**
* Gets the current position
* @param call the plugin call
*/
private fun getPosition(call: PluginCall) {
coroutineScope.launch {
val locationOptions = createOptions(call)
// call getCurrentPosition method from controller
val locationResult = controller.getCurrentPosition(activity, locationOptions)
locationResult
.onSuccess { location ->
call.sendSuccess(getJSObjectForLocation(location))
}
.onFailure { exception ->
onLocationError(exception, call)
}
}
}
/**
* Starts watching the device's location by requesting location updates
* @param call the plugin call
*/
private fun startWatch(call: PluginCall) {
coroutineScope.launch {
val watchId = call.callbackId
val locationOptions = createOptions(call)
// call addWatch method from controller
controller.addWatch(activity, locationOptions, watchId).collect { result ->
result.onSuccess { locationList ->
locationList.forEach { locationResult ->
call.sendSuccess(
result = getJSObjectForLocation(locationResult),
keepCallback = true)
}
}
result.onFailure { exception ->
onLocationError(exception, call)
}
}
}
watchingCalls[call.callbackId] = call
}
/**
* Helper function to convert IONGLOCLocationResult object into the format accepted by the Capacitor bridge
* @param locationResult IONGLOCLocationResult object with the location to convert
* @return JSObject with converted JSON object
*/
private fun getJSObjectForLocation(locationResult: IONGLOCLocationResult): JSObject {
val coords = JSObject().apply {
put("latitude", locationResult.latitude)
put("longitude", locationResult.longitude)
put("accuracy", locationResult.accuracy)
put("altitude", locationResult.altitude)
locationResult.altitudeAccuracy?.let { put("altitudeAccuracy", it) }
put("speed", locationResult.speed)
put("heading", locationResult.heading)
}
return JSObject().apply {
put("timestamp", locationResult.timestamp)
put("coords", coords)
}
}
/**
* Helper function to handle error cases
* @param exception Throwable to handle as an error
* @param call the plugin call
*/
private fun onLocationError(exception: Throwable?, call: PluginCall) {
when (exception) {
is IONGLOCException.IONGLOCRequestDeniedException -> {
call.sendError(GeolocationErrors.LOCATION_ENABLE_REQUEST_DENIED)
}
is IONGLOCException.IONGLOCSettingsException -> {
call.sendError(GeolocationErrors.LOCATION_SETTINGS_ERROR)
}
is IONGLOCException.IONGLOCInvalidTimeoutException -> {
call.sendError(GeolocationErrors.INVALID_TIMEOUT)
}
is IONGLOCException.IONGLOCGoogleServicesException -> {
if (exception.resolvable) {
call.sendError(GeolocationErrors.GOOGLE_SERVICES_RESOLVABLE)
} else {
call.sendError(GeolocationErrors.GOOGLE_SERVICES_ERROR)
}
}
is IONGLOCException.IONGLOCLocationRetrievalTimeoutException -> {
call.sendError(GeolocationErrors.GET_LOCATION_TIMEOUT)
}
else -> {
call.sendError(GeolocationErrors.POSITION_UNAVAILABLE)
}
}
}
/**
* Extension function to return a successful plugin result
* @param result JSOObject with the JSON content to return
* @param keepCallback boolean to determine if callback should be kept for future calls or not
*/
private fun PluginCall.sendSuccess(result: JSObject? = null, keepCallback: Boolean? = false) {
this.setKeepAlive(keepCallback)
if (result != null) {
this.resolve(result)
} else {
this.resolve()
}
}
/**
* Extension function to return a unsuccessful plugin result
* @param error error class representing the error to return, containing a code and message
*/
private fun PluginCall.sendError(error: GeolocationErrors.ErrorInfo) {
this.reject(error.message, error.code)
}
/**
* Creates the location options to pass to the native controller
* @param call the plugin call
* @return IONGLOCLocationOptions object
*/
private fun createOptions(call: PluginCall): IONGLOCLocationOptions {
val timeout = call.getNumber("timeout", 10000)
val maximumAge = call.getNumber("maximumAge", 0)
val enableHighAccuracy = call.getBoolean("enableHighAccuracy", false) ?: false
val minimumUpdateInterval = call.getNumber("minimumUpdateInterval", 5000)
val locationOptions = IONGLOCLocationOptions(timeout, maximumAge, enableHighAccuracy, minimumUpdateInterval)
return locationOptions
}
private fun PluginCall.getNumber(name: String, defaultValue: Long): Long =
getLong(name) ?: getInt(name)?.toLong() ?: defaultValue
}

View File