@detonator/location 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/DetonatorLocation/build.gradle +38 -0
- package/android/DetonatorLocation/consumer-rules.pro +0 -0
- package/android/DetonatorLocation/proguard-rules.pro +21 -0
- package/android/DetonatorLocation/src/androidTest/java/com/iconshot/detonator/location/ExampleInstrumentedTest.java +26 -0
- package/android/DetonatorLocation/src/main/AndroidManifest.xml +11 -0
- package/android/DetonatorLocation/src/main/java/com/iconshot/detonator/location/LocationActivity.java +58 -0
- package/android/DetonatorLocation/src/main/java/com/iconshot/detonator/location/LocationModule.java +155 -0
- package/android/DetonatorLocation/src/test/java/com/iconshot/detonator/location/ExampleUnitTest.java +17 -0
- package/dist/esm/Location.d.ts +23 -0
- package/dist/esm/Location.js +21 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/ios/DetonatorLocation/Package.swift +28 -0
- package/ios/DetonatorLocation/Sources/DetonatorLocation/LocationGetter.swift +124 -0
- package/ios/DetonatorLocation/Sources/DetonatorLocation/LocationModule.swift +154 -0
- package/package.json +31 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
alias(libs.plugins.androidLibrary)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
android {
|
|
6
|
+
namespace 'com.iconshot.detonator.location'
|
|
7
|
+
compileSdk 34
|
|
8
|
+
|
|
9
|
+
defaultConfig {
|
|
10
|
+
minSdk 29
|
|
11
|
+
|
|
12
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
13
|
+
consumerProguardFiles "consumer-rules.pro"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
buildTypes {
|
|
17
|
+
release {
|
|
18
|
+
minifyEnabled false
|
|
19
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
compileOptions {
|
|
23
|
+
sourceCompatibility JavaVersion.VERSION_11
|
|
24
|
+
targetCompatibility JavaVersion.VERSION_11
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dependencies {
|
|
29
|
+
implementation project(':Detonator')
|
|
30
|
+
|
|
31
|
+
implementation "com.google.android.gms:play-services-location:21.3.0"
|
|
32
|
+
|
|
33
|
+
implementation libs.appcompat
|
|
34
|
+
implementation libs.material
|
|
35
|
+
testImplementation libs.junit
|
|
36
|
+
androidTestImplementation libs.ext.junit
|
|
37
|
+
androidTestImplementation libs.espresso.core
|
|
38
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Add project specific ProGuard rules here.
|
|
2
|
+
# You can control the set of applied configuration files using the
|
|
3
|
+
# proguardFiles setting in build.gradle.
|
|
4
|
+
#
|
|
5
|
+
# For more details, see
|
|
6
|
+
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
7
|
+
|
|
8
|
+
# If your project uses WebView with JS, uncomment the following
|
|
9
|
+
# and specify the fully qualified class name to the JavaScript interface
|
|
10
|
+
# class:
|
|
11
|
+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
12
|
+
# public *;
|
|
13
|
+
#}
|
|
14
|
+
|
|
15
|
+
# Uncomment this to preserve the line number information for
|
|
16
|
+
# debugging stack traces.
|
|
17
|
+
#-keepattributes SourceFile,LineNumberTable
|
|
18
|
+
|
|
19
|
+
# If you keep the line number information, uncomment this to
|
|
20
|
+
# hide the original source file name.
|
|
21
|
+
#-renamesourcefileattribute SourceFile
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.iconshot.detonator.location;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
|
|
5
|
+
import androidx.test.platform.app.InstrumentationRegistry;
|
|
6
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
7
|
+
|
|
8
|
+
import org.junit.Test;
|
|
9
|
+
import org.junit.runner.RunWith;
|
|
10
|
+
|
|
11
|
+
import static org.junit.Assert.*;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Instrumented test, which will execute on an Android device.
|
|
15
|
+
*
|
|
16
|
+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
17
|
+
*/
|
|
18
|
+
@RunWith(AndroidJUnit4.class)
|
|
19
|
+
public class ExampleInstrumentedTest {
|
|
20
|
+
@Test
|
|
21
|
+
public void useAppContext() {
|
|
22
|
+
// Context of the app under test.
|
|
23
|
+
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
24
|
+
assertEquals("com.iconshot.detonator.location.test", appContext.getPackageName());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
4
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
5
|
+
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
6
|
+
|
|
7
|
+
<application>
|
|
8
|
+
<activity android:name=".LocationActivity"
|
|
9
|
+
android:exported="false"/>
|
|
10
|
+
</application>
|
|
11
|
+
</manifest>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package com.iconshot.detonator.location;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.app.Activity;
|
|
5
|
+
import android.content.pm.PackageManager;
|
|
6
|
+
import android.os.Bundle;
|
|
7
|
+
|
|
8
|
+
import androidx.annotation.NonNull;
|
|
9
|
+
import androidx.core.app.ActivityCompat;
|
|
10
|
+
|
|
11
|
+
import java.util.function.Consumer;
|
|
12
|
+
|
|
13
|
+
public class LocationActivity extends Activity {
|
|
14
|
+
public static Consumer<Boolean> permissionResultCallback;
|
|
15
|
+
|
|
16
|
+
public static Boolean background;
|
|
17
|
+
|
|
18
|
+
@Override
|
|
19
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
20
|
+
super.onCreate(savedInstanceState);
|
|
21
|
+
|
|
22
|
+
// activities get recreated on permission downgrade (e.g. Allow in use -> Don't allow)
|
|
23
|
+
|
|
24
|
+
if (background == null) {
|
|
25
|
+
finish();
|
|
26
|
+
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (background) {
|
|
31
|
+
ActivityCompat.requestPermissions(
|
|
32
|
+
this,
|
|
33
|
+
new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
|
34
|
+
100
|
|
35
|
+
);
|
|
36
|
+
} else {
|
|
37
|
+
ActivityCompat.requestPermissions(
|
|
38
|
+
this,
|
|
39
|
+
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
|
40
|
+
100
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Override
|
|
46
|
+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
|
47
|
+
@NonNull int[] grantResults) {
|
|
48
|
+
boolean hasGrantedLocationPermission = grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
|
49
|
+
|
|
50
|
+
boolean permissionsGranted = hasGrantedLocationPermission;
|
|
51
|
+
|
|
52
|
+
if (permissionResultCallback != null) {
|
|
53
|
+
permissionResultCallback.accept(permissionsGranted);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
finish();
|
|
57
|
+
}
|
|
58
|
+
}
|
package/android/DetonatorLocation/src/main/java/com/iconshot/detonator/location/LocationModule.java
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
package com.iconshot.detonator.location;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.content.Intent;
|
|
5
|
+
import android.content.pm.PackageManager;
|
|
6
|
+
|
|
7
|
+
import androidx.core.content.ContextCompat;
|
|
8
|
+
|
|
9
|
+
import com.google.android.gms.location.FusedLocationProviderClient;
|
|
10
|
+
import com.google.android.gms.location.LocationServices;
|
|
11
|
+
import com.google.android.gms.location.Priority;
|
|
12
|
+
import com.google.android.gms.tasks.CancellationTokenSource;
|
|
13
|
+
import com.iconshot.detonator.Detonator;
|
|
14
|
+
import com.iconshot.detonator.module.Module;
|
|
15
|
+
|
|
16
|
+
public class LocationModule extends Module {
|
|
17
|
+
private FusedLocationProviderClient locationClient;
|
|
18
|
+
|
|
19
|
+
public LocationModule(Detonator detonator) {
|
|
20
|
+
super(detonator);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Override
|
|
24
|
+
public void setUp() {
|
|
25
|
+
locationClient = LocationServices.getFusedLocationProviderClient(detonator.context);
|
|
26
|
+
|
|
27
|
+
detonator.setRequestListener("com.iconshot.detonator.location::requestPermission", (promise, value, edge) -> {
|
|
28
|
+
RequestPermissionOptions options = detonator.decode(value, RequestPermissionOptions.class);
|
|
29
|
+
|
|
30
|
+
boolean permissionGranted = options.background
|
|
31
|
+
? ContextCompat.checkSelfPermission(
|
|
32
|
+
detonator.context,
|
|
33
|
+
Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
|
34
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
35
|
+
: ContextCompat.checkSelfPermission(
|
|
36
|
+
detonator.context,
|
|
37
|
+
Manifest.permission.ACCESS_FINE_LOCATION
|
|
38
|
+
) == PackageManager.PERMISSION_GRANTED;
|
|
39
|
+
|
|
40
|
+
if (permissionGranted) {
|
|
41
|
+
promise.resolve(true);
|
|
42
|
+
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
LocationActivity.background = options.background;
|
|
47
|
+
|
|
48
|
+
LocationActivity.permissionResultCallback = granted -> {
|
|
49
|
+
promise.resolve(granted);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
detonator.context.startActivity(new Intent(detonator.context, LocationActivity.class));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
detonator.setRequestListener("com.iconshot.detonator.location::getLocation", (promise, value, edge) -> {
|
|
56
|
+
GetLocationOptions options = detonator.decode(value, GetLocationOptions.class);
|
|
57
|
+
|
|
58
|
+
CancellationTokenSource token = new CancellationTokenSource();
|
|
59
|
+
|
|
60
|
+
Runnable timeoutRunnable = () -> {
|
|
61
|
+
token.cancel();
|
|
62
|
+
|
|
63
|
+
promise.reject("Location request timed out.");
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
detonator.uiHandler.postDelayed(timeoutRunnable, options.timeout);
|
|
67
|
+
|
|
68
|
+
locationClient.getCurrentLocation(
|
|
69
|
+
options.accurate
|
|
70
|
+
? Priority.PRIORITY_HIGH_ACCURACY
|
|
71
|
+
: Priority.PRIORITY_BALANCED_POWER_ACCURACY,
|
|
72
|
+
token.getToken()
|
|
73
|
+
).addOnSuccessListener(location -> {
|
|
74
|
+
detonator.uiHandler.removeCallbacks(timeoutRunnable);
|
|
75
|
+
|
|
76
|
+
token.cancel();
|
|
77
|
+
|
|
78
|
+
if (location == null) {
|
|
79
|
+
promise.reject("Location not available.");
|
|
80
|
+
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!location.hasAccuracy()) {
|
|
85
|
+
promise.reject("Location lacks accuracy.");
|
|
86
|
+
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (location.getAccuracy() > options.maxAccuracy) {
|
|
91
|
+
promise.reject("Location not accurate enough.");
|
|
92
|
+
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
long age = System.currentTimeMillis() - location.getTime();
|
|
97
|
+
|
|
98
|
+
if (age > options.maxAge) {
|
|
99
|
+
promise.reject("Location too old.");
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
LocationData data = new LocationData();
|
|
105
|
+
|
|
106
|
+
data.latitude = location.getLatitude();
|
|
107
|
+
data.longitude = location.getLongitude();
|
|
108
|
+
data.altitude = location.hasAltitude() ? location.getAltitude() : null;
|
|
109
|
+
data.accuracy = location.hasAccuracy() ? location.getAccuracy() : null;
|
|
110
|
+
data.speed = location.hasSpeed() ? location.getSpeed() : null;
|
|
111
|
+
data.time = location.getTime();
|
|
112
|
+
data.bearing = location.hasBearing() ? location.getBearing() : null;
|
|
113
|
+
data.provider = location.getProvider();
|
|
114
|
+
|
|
115
|
+
data.verticalAccuracy = location.hasVerticalAccuracy()
|
|
116
|
+
? location.getVerticalAccuracyMeters()
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
data.course = data.bearing;
|
|
120
|
+
|
|
121
|
+
promise.resolve(data);
|
|
122
|
+
}).addOnFailureListener(error -> {
|
|
123
|
+
detonator.uiHandler.removeCallbacks(timeoutRunnable);
|
|
124
|
+
|
|
125
|
+
token.cancel();
|
|
126
|
+
|
|
127
|
+
promise.reject("Location not available.");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public static class RequestPermissionOptions {
|
|
133
|
+
boolean background;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public static class GetLocationOptions {
|
|
137
|
+
boolean accurate;
|
|
138
|
+
int maxAge;
|
|
139
|
+
double maxAccuracy;
|
|
140
|
+
int timeout;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public static class LocationData {
|
|
144
|
+
double latitude;
|
|
145
|
+
double longitude;
|
|
146
|
+
Double altitude;
|
|
147
|
+
Float accuracy;
|
|
148
|
+
Float speed;
|
|
149
|
+
long time;
|
|
150
|
+
Float bearing;
|
|
151
|
+
String provider;
|
|
152
|
+
Float verticalAccuracy;
|
|
153
|
+
Float course;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/android/DetonatorLocation/src/test/java/com/iconshot/detonator/location/ExampleUnitTest.java
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.iconshot.detonator.location;
|
|
2
|
+
|
|
3
|
+
import org.junit.Test;
|
|
4
|
+
|
|
5
|
+
import static org.junit.Assert.*;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Example local unit test, which will execute on the development machine (host).
|
|
9
|
+
*
|
|
10
|
+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
11
|
+
*/
|
|
12
|
+
public class ExampleUnitTest {
|
|
13
|
+
@Test
|
|
14
|
+
public void addition_isCorrect() {
|
|
15
|
+
assertEquals(4, 2 + 2);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface LocationData {
|
|
2
|
+
latitude: number;
|
|
3
|
+
longitude: number;
|
|
4
|
+
altitude: number | null;
|
|
5
|
+
accuracy: number | null;
|
|
6
|
+
speed: number | null;
|
|
7
|
+
time: number;
|
|
8
|
+
bearing: number | null;
|
|
9
|
+
provider: string | null;
|
|
10
|
+
verticalAccuracy: number | null;
|
|
11
|
+
course: number | null;
|
|
12
|
+
}
|
|
13
|
+
export declare class Location {
|
|
14
|
+
static requestPermission(options?: Partial<{
|
|
15
|
+
background: boolean;
|
|
16
|
+
}>): Promise<boolean>;
|
|
17
|
+
static getLocation(options?: Partial<{
|
|
18
|
+
accurate: boolean;
|
|
19
|
+
maxAge: number;
|
|
20
|
+
maxAccuracy: number;
|
|
21
|
+
timeout: number;
|
|
22
|
+
}>): Promise<LocationData>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Detonator, { Platform } from "detonator";
|
|
2
|
+
export class Location {
|
|
3
|
+
static async requestPermission(options = {}) {
|
|
4
|
+
const tmpOptions = { ...options };
|
|
5
|
+
tmpOptions.background = options.background ?? false;
|
|
6
|
+
return await Detonator.request("com.iconshot.detonator.location::requestPermission", tmpOptions).fetchAndDecode();
|
|
7
|
+
}
|
|
8
|
+
static async getLocation(options = {}) {
|
|
9
|
+
const tmpOptions = { ...options };
|
|
10
|
+
tmpOptions.accurate = options.accurate ?? false;
|
|
11
|
+
if (Platform.get() === "ios") {
|
|
12
|
+
tmpOptions.accurate = true;
|
|
13
|
+
}
|
|
14
|
+
tmpOptions.maxAge = options.maxAge ?? 15000;
|
|
15
|
+
tmpOptions.maxAccuracy =
|
|
16
|
+
options.maxAccuracy ?? (tmpOptions.accurate ? 60 : 200);
|
|
17
|
+
tmpOptions.timeout =
|
|
18
|
+
options.timeout ?? (tmpOptions.accurate ? 20000 : 10000);
|
|
19
|
+
return await Detonator.request("com.iconshot.detonator.location::getLocation", tmpOptions).fetchAndDecode();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Location";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Location";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// swift-tools-version: 5.10
|
|
2
|
+
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
3
|
+
|
|
4
|
+
import PackageDescription
|
|
5
|
+
|
|
6
|
+
let package = Package(
|
|
7
|
+
name: "DetonatorLocation",
|
|
8
|
+
platforms: [
|
|
9
|
+
.iOS(.v13),
|
|
10
|
+
],
|
|
11
|
+
products: [
|
|
12
|
+
// Products define the executables and libraries a package produces, making them visible to other packages.
|
|
13
|
+
.library(
|
|
14
|
+
name: "DetonatorLocation",
|
|
15
|
+
targets: ["DetonatorLocation"]),
|
|
16
|
+
],
|
|
17
|
+
dependencies: [
|
|
18
|
+
.package(name: "Detonator", path: "../../../../../node_modules/detonator/ios/Detonator")
|
|
19
|
+
],
|
|
20
|
+
targets: [
|
|
21
|
+
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
|
22
|
+
// Targets can depend on other targets in this package and products from dependencies.
|
|
23
|
+
.target(
|
|
24
|
+
name: "DetonatorLocation",
|
|
25
|
+
dependencies: ["Detonator"]),
|
|
26
|
+
|
|
27
|
+
]
|
|
28
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
|
|
3
|
+
import Detonator
|
|
4
|
+
|
|
5
|
+
public class LocationGetter: NSObject, CLLocationManagerDelegate {
|
|
6
|
+
private let locationManager = CLLocationManager()
|
|
7
|
+
|
|
8
|
+
private let promise: RequestPromise
|
|
9
|
+
private let options: GetLocationOptions
|
|
10
|
+
|
|
11
|
+
private var timeoutTimer: Timer?
|
|
12
|
+
private var fulfillTimer: Timer?
|
|
13
|
+
|
|
14
|
+
private var ended: Bool = false
|
|
15
|
+
|
|
16
|
+
public var onEnded: (() -> Void)!
|
|
17
|
+
|
|
18
|
+
init(_ promise: RequestPromise, _ options: GetLocationOptions) {
|
|
19
|
+
self.promise = promise
|
|
20
|
+
self.options = options
|
|
21
|
+
|
|
22
|
+
super.init()
|
|
23
|
+
|
|
24
|
+
locationManager.delegate = self
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public func run() -> Void {
|
|
28
|
+
timeoutTimer = Timer.scheduledTimer(
|
|
29
|
+
withTimeInterval: TimeInterval(options.timeout) / 1000.0,
|
|
30
|
+
repeats: false
|
|
31
|
+
) { _ in
|
|
32
|
+
self.timeoutTimer?.invalidate()
|
|
33
|
+
|
|
34
|
+
self.promise.reject("Location request timed out.")
|
|
35
|
+
|
|
36
|
+
self.end()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
locationManager.requestLocation()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func end() -> Void {
|
|
43
|
+
ended = true
|
|
44
|
+
|
|
45
|
+
onEnded()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
49
|
+
if ended {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
timeoutTimer?.invalidate()
|
|
54
|
+
|
|
55
|
+
fulfillTimer?.invalidate()
|
|
56
|
+
|
|
57
|
+
fulfillTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
|
58
|
+
guard let location = locations.last else {
|
|
59
|
+
self.promise.reject("Location not available.")
|
|
60
|
+
|
|
61
|
+
self.end()
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if location.horizontalAccuracy < 0 {
|
|
67
|
+
self.promise.reject("Location lacks accuracy.")
|
|
68
|
+
|
|
69
|
+
self.end()
|
|
70
|
+
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if Double(location.horizontalAccuracy) > self.options.maxAccuracy {
|
|
75
|
+
self.promise.reject("Location not accurate enough.")
|
|
76
|
+
|
|
77
|
+
self.end()
|
|
78
|
+
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let age = Int(Date().timeIntervalSince1970 * 1000) - Int(location.timestamp.timeIntervalSince1970 * 1000)
|
|
83
|
+
|
|
84
|
+
if age > self.options.maxAge {
|
|
85
|
+
self.promise.reject("Location too old.")
|
|
86
|
+
|
|
87
|
+
self.end()
|
|
88
|
+
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let data = LocationData(
|
|
93
|
+
latitude: location.coordinate.latitude,
|
|
94
|
+
longitude: location.coordinate.longitude,
|
|
95
|
+
altitude: location.verticalAccuracy < 0 ? nil : location.altitude,
|
|
96
|
+
accuracy: location.horizontalAccuracy,
|
|
97
|
+
speed: location.speed >= 0 ? location.speed : nil,
|
|
98
|
+
time: Int(location.timestamp.timeIntervalSince1970 * 1000),
|
|
99
|
+
bearing: location.course >= 0 ? location.course : nil,
|
|
100
|
+
provider: "Apple",
|
|
101
|
+
verticalAccuracy: location.verticalAccuracy >= 0 ? location.verticalAccuracy : nil,
|
|
102
|
+
course: location.course >= 0 ? location.course : nil,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.promise.resolve(data)
|
|
106
|
+
|
|
107
|
+
self.end()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// either update or failure will happen, not both
|
|
112
|
+
|
|
113
|
+
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
114
|
+
if ended {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
timeoutTimer?.invalidate()
|
|
119
|
+
|
|
120
|
+
promise.reject("Location not available.")
|
|
121
|
+
|
|
122
|
+
end()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import CoreLocation
|
|
4
|
+
|
|
5
|
+
import Detonator
|
|
6
|
+
|
|
7
|
+
public class LocationModule: Module, CLLocationManagerDelegate {
|
|
8
|
+
private let locationManager = CLLocationManager()
|
|
9
|
+
|
|
10
|
+
private var requestPermissionPromise: RequestPromise?
|
|
11
|
+
private var requestPermissionBackground: Bool?
|
|
12
|
+
private var requestPermissionTimer: Timer?
|
|
13
|
+
|
|
14
|
+
private var locationGetters: [LocationGetter] = []
|
|
15
|
+
|
|
16
|
+
public override func setUp() -> Void {
|
|
17
|
+
locationManager.delegate = self
|
|
18
|
+
|
|
19
|
+
NotificationCenter.default.addObserver(
|
|
20
|
+
self,
|
|
21
|
+
selector: #selector(self.permissionDialogOpened),
|
|
22
|
+
name: UIApplication.willResignActiveNotification,
|
|
23
|
+
object: nil
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
NotificationCenter.default.addObserver(
|
|
27
|
+
self,
|
|
28
|
+
selector: #selector(self.permissionDialogClosed),
|
|
29
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
30
|
+
object: nil
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
detonator.setRequestListener("com.iconshot.detonator.location::requestPermission") { promise, value, edge in
|
|
34
|
+
let options: RequestPermissionOptions = self.detonator.decode(value)!
|
|
35
|
+
|
|
36
|
+
let status = CLLocationManager.authorizationStatus()
|
|
37
|
+
|
|
38
|
+
if options.background {
|
|
39
|
+
if status == .authorizedAlways {
|
|
40
|
+
promise.resolve(true)
|
|
41
|
+
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
if status == .authorizedWhenInUse || status == .authorizedAlways {
|
|
46
|
+
promise.resolve(true)
|
|
47
|
+
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if status == .denied || status == .restricted {
|
|
53
|
+
promise.resolve(false)
|
|
54
|
+
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
self.requestPermissionPromise = promise
|
|
59
|
+
self.requestPermissionBackground = options.background
|
|
60
|
+
|
|
61
|
+
if options.background {
|
|
62
|
+
self.locationManager.requestAlwaysAuthorization()
|
|
63
|
+
|
|
64
|
+
self.requestPermissionTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
|
|
65
|
+
self.resolveBackgroundRequestPermissionIfDialogNotOpened()
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
self.locationManager.requestWhenInUseAuthorization()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
detonator.setRequestListener("com.iconshot.detonator.location::getLocation") { promise, value, edge in
|
|
73
|
+
let options: GetLocationOptions = self.detonator.decode(value)!
|
|
74
|
+
|
|
75
|
+
let locationGetter = LocationGetter(promise, options)
|
|
76
|
+
|
|
77
|
+
self.locationGetters.append(locationGetter)
|
|
78
|
+
|
|
79
|
+
locationGetter.onEnded = {
|
|
80
|
+
self.locationGetters.removeAll { $0 === locationGetter }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
locationGetter.run()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@objc private func permissionDialogOpened() {
|
|
88
|
+
guard let requestPermissionPromise = self.requestPermissionPromise else {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
self.requestPermissionTimer?.invalidate()
|
|
93
|
+
|
|
94
|
+
self.requestPermissionTimer = nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@objc private func permissionDialogClosed() {
|
|
98
|
+
guard
|
|
99
|
+
let requestPermissionPromise = self.requestPermissionPromise,
|
|
100
|
+
let requestPermissionBackground = self.requestPermissionBackground
|
|
101
|
+
else {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let status = CLLocationManager.authorizationStatus()
|
|
106
|
+
|
|
107
|
+
let granted = requestPermissionBackground
|
|
108
|
+
? status == .authorizedAlways
|
|
109
|
+
: status == .authorizedWhenInUse || status == .authorizedAlways
|
|
110
|
+
|
|
111
|
+
requestPermissionPromise.resolve(granted)
|
|
112
|
+
|
|
113
|
+
self.requestPermissionPromise = nil
|
|
114
|
+
self.requestPermissionBackground = nil
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func resolveBackgroundRequestPermissionIfDialogNotOpened() {
|
|
118
|
+
guard let requestPermissionPromise = self.requestPermissionPromise else {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
requestPermissionPromise.resolve(false)
|
|
123
|
+
|
|
124
|
+
self.requestPermissionTimer?.invalidate()
|
|
125
|
+
|
|
126
|
+
self.requestPermissionPromise = nil
|
|
127
|
+
self.requestPermissionBackground = nil
|
|
128
|
+
self.requestPermissionTimer = nil
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private struct RequestPermissionOptions: Decodable {
|
|
133
|
+
let background: Bool
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public struct GetLocationOptions: Decodable {
|
|
137
|
+
let accurate: Bool
|
|
138
|
+
let maxAge: Int
|
|
139
|
+
let maxAccuracy: Double
|
|
140
|
+
let timeout: Int
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public struct LocationData: Encodable {
|
|
144
|
+
let latitude: Double
|
|
145
|
+
let longitude: Double
|
|
146
|
+
let altitude: Double?
|
|
147
|
+
let accuracy: Double?
|
|
148
|
+
let speed: Double?
|
|
149
|
+
let time: Int
|
|
150
|
+
let bearing: Double?
|
|
151
|
+
let provider: String?
|
|
152
|
+
let verticalAccuracy: Double?
|
|
153
|
+
let course: Double?
|
|
154
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@detonator/location",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Location module for Detonator.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/iconshot/detonator-location.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/esm/index.js",
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"remove-dist": "rm -rf dist",
|
|
14
|
+
"bundle-dist": "npm run remove-dist && tsc",
|
|
15
|
+
"build": "npm run bundle-dist",
|
|
16
|
+
"prepare": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"location",
|
|
20
|
+
"detonator",
|
|
21
|
+
"untrue"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"detonator": "^1.11.2",
|
|
26
|
+
"untrue": "^5.18.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|