foreground-location 0.0.1
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/ForegroundLocation.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +931 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +30 -0
- package/android/src/main/java/in/xconcepts/foreground/location/APIService.java +449 -0
- package/android/src/main/java/in/xconcepts/foreground/location/ForeGroundLocation.java +58 -0
- package/android/src/main/java/in/xconcepts/foreground/location/ForeGroundLocationPlugin.java +650 -0
- package/android/src/main/java/in/xconcepts/foreground/location/LocationForegroundService.java +526 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/esm/definitions.d.ts +240 -0
- package/dist/esm/definitions.js +26 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +16 -0
- package/dist/esm/web.js +97 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +138 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +141 -0
- package/dist/plugin.js.map +1 -0
- package/docs/CHANGES-SUMMARY.md +69 -0
- package/docs/setup-and-examples.md +2851 -0
- package/ios/Sources/ForeGroundLocationPlugin/ForeGroundLocation.swift +75 -0
- package/ios/Sources/ForeGroundLocationPlugin/ForeGroundLocationPlugin.swift +125 -0
- package/ios/Tests/ForeGroundLocationPluginTests/ForeGroundLocationPluginTests.swift +36 -0
- package/package.json +82 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
package in.xconcepts.foreground.location;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.content.BroadcastReceiver;
|
|
5
|
+
import android.content.ComponentName;
|
|
6
|
+
import android.content.Context;
|
|
7
|
+
import android.content.Intent;
|
|
8
|
+
import android.content.IntentFilter;
|
|
9
|
+
import android.content.ServiceConnection;
|
|
10
|
+
import android.content.pm.PackageManager;
|
|
11
|
+
import android.location.Location;
|
|
12
|
+
import android.os.Build;
|
|
13
|
+
import android.os.IBinder;
|
|
14
|
+
import android.util.Log;
|
|
15
|
+
|
|
16
|
+
import androidx.core.content.ContextCompat;
|
|
17
|
+
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
18
|
+
|
|
19
|
+
import com.getcapacitor.JSObject;
|
|
20
|
+
import com.getcapacitor.PermissionState;
|
|
21
|
+
import com.getcapacitor.Plugin;
|
|
22
|
+
import com.getcapacitor.PluginCall;
|
|
23
|
+
import com.getcapacitor.PluginMethod;
|
|
24
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
25
|
+
import com.getcapacitor.annotation.Permission;
|
|
26
|
+
import com.getcapacitor.annotation.PermissionCallback;
|
|
27
|
+
|
|
28
|
+
import com.google.android.gms.location.FusedLocationProviderClient;
|
|
29
|
+
import com.google.android.gms.location.LocationServices;
|
|
30
|
+
|
|
31
|
+
@CapacitorPlugin(
|
|
32
|
+
name = "ForeGroundLocation",
|
|
33
|
+
permissions = {
|
|
34
|
+
@Permission(
|
|
35
|
+
alias = "location",
|
|
36
|
+
strings = {
|
|
37
|
+
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
38
|
+
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
@Permission(
|
|
42
|
+
alias = "backgroundLocation",
|
|
43
|
+
strings = { Manifest.permission.ACCESS_BACKGROUND_LOCATION }
|
|
44
|
+
),
|
|
45
|
+
@Permission(
|
|
46
|
+
alias = "notifications",
|
|
47
|
+
strings = { Manifest.permission.POST_NOTIFICATIONS }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
public class ForeGroundLocationPlugin extends Plugin {
|
|
52
|
+
private static final String TAG = "ForeGroundLocationPlugin";
|
|
53
|
+
|
|
54
|
+
private ForeGroundLocation implementation = new ForeGroundLocation();
|
|
55
|
+
private LocationForegroundService locationService;
|
|
56
|
+
private boolean isServiceBound = false;
|
|
57
|
+
private FusedLocationProviderClient fusedLocationClient;
|
|
58
|
+
|
|
59
|
+
// Broadcast receiver for location updates
|
|
60
|
+
private BroadcastReceiver locationUpdateReceiver = new BroadcastReceiver() {
|
|
61
|
+
@Override
|
|
62
|
+
public void onReceive(Context context, Intent intent) {
|
|
63
|
+
if (LocationForegroundService.ACTION_LOCATION_UPDATE.equals(intent.getAction())) {
|
|
64
|
+
JSObject locationData = new JSObject();
|
|
65
|
+
locationData.put("latitude", intent.getDoubleExtra("latitude", 0.0));
|
|
66
|
+
locationData.put("longitude", intent.getDoubleExtra("longitude", 0.0));
|
|
67
|
+
locationData.put("accuracy", intent.getDoubleExtra("accuracy", 0.0));
|
|
68
|
+
locationData.put("timestamp", intent.getStringExtra("timestamp"));
|
|
69
|
+
|
|
70
|
+
if (intent.hasExtra("altitude")) {
|
|
71
|
+
locationData.put("altitude", intent.getDoubleExtra("altitude", 0.0));
|
|
72
|
+
}
|
|
73
|
+
if (intent.hasExtra("bearing")) {
|
|
74
|
+
locationData.put("bearing", intent.getDoubleExtra("bearing", 0.0));
|
|
75
|
+
}
|
|
76
|
+
if (intent.hasExtra("speed")) {
|
|
77
|
+
locationData.put("speed", intent.getDoubleExtra("speed", 0.0));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
notifyListeners("locationUpdate", locationData);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Broadcast receiver for service status
|
|
86
|
+
private BroadcastReceiver serviceStatusReceiver = new BroadcastReceiver() {
|
|
87
|
+
@Override
|
|
88
|
+
public void onReceive(Context context, Intent intent) {
|
|
89
|
+
if (LocationForegroundService.ACTION_SERVICE_STATUS.equals(intent.getAction())) {
|
|
90
|
+
JSObject statusData = new JSObject();
|
|
91
|
+
statusData.put("isRunning", intent.getBooleanExtra(LocationForegroundService.EXTRA_SERVICE_STATUS, false));
|
|
92
|
+
|
|
93
|
+
String error = intent.getStringExtra(LocationForegroundService.EXTRA_ERROR_MESSAGE);
|
|
94
|
+
if (error != null) {
|
|
95
|
+
statusData.put("error", error);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
notifyListeners("serviceStatusChanged", statusData);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Service connection
|
|
104
|
+
private ServiceConnection serviceConnection = new ServiceConnection() {
|
|
105
|
+
@Override
|
|
106
|
+
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
107
|
+
LocationForegroundService.LocationServiceBinder binder =
|
|
108
|
+
(LocationForegroundService.LocationServiceBinder) service;
|
|
109
|
+
locationService = binder.getService();
|
|
110
|
+
isServiceBound = true;
|
|
111
|
+
Log.d(TAG, "Service connected");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Override
|
|
115
|
+
public void onServiceDisconnected(ComponentName name) {
|
|
116
|
+
locationService = null;
|
|
117
|
+
isServiceBound = false;
|
|
118
|
+
Log.d(TAG, "Service disconnected");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
@Override
|
|
123
|
+
public void load() {
|
|
124
|
+
fusedLocationClient = LocationServices.getFusedLocationProviderClient(getContext());
|
|
125
|
+
|
|
126
|
+
// Unregister existing receivers first (safety)
|
|
127
|
+
try {
|
|
128
|
+
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(locationUpdateReceiver);
|
|
129
|
+
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(serviceStatusReceiver);
|
|
130
|
+
} catch (Exception e) {
|
|
131
|
+
// Ignore if not registered
|
|
132
|
+
Log.d(TAG, "No existing receivers to unregister");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Register broadcast receivers
|
|
136
|
+
LocalBroadcastManager.getInstance(getContext()).registerReceiver(
|
|
137
|
+
locationUpdateReceiver,
|
|
138
|
+
new IntentFilter(LocationForegroundService.ACTION_LOCATION_UPDATE)
|
|
139
|
+
);
|
|
140
|
+
LocalBroadcastManager.getInstance(getContext()).registerReceiver(
|
|
141
|
+
serviceStatusReceiver,
|
|
142
|
+
new IntentFilter(LocationForegroundService.ACTION_SERVICE_STATUS)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
Log.d(TAG, "Plugin loaded and receivers registered");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Override
|
|
149
|
+
protected void handleOnDestroy() {
|
|
150
|
+
// Unregister broadcast receivers safely
|
|
151
|
+
try {
|
|
152
|
+
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(locationUpdateReceiver);
|
|
153
|
+
} catch (Exception e) {
|
|
154
|
+
Log.w(TAG, "Error unregistering locationUpdateReceiver", e);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(serviceStatusReceiver);
|
|
159
|
+
} catch (Exception e) {
|
|
160
|
+
Log.w(TAG, "Error unregistering serviceStatusReceiver", e);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Unbind service safely
|
|
164
|
+
if (isServiceBound) {
|
|
165
|
+
try {
|
|
166
|
+
getContext().unbindService(serviceConnection);
|
|
167
|
+
isServiceBound = false;
|
|
168
|
+
Log.d(TAG, "Service unbound successfully");
|
|
169
|
+
} catch (Exception e) {
|
|
170
|
+
Log.w(TAG, "Error unbinding service", e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Clean up references
|
|
175
|
+
locationService = null;
|
|
176
|
+
fusedLocationClient = null;
|
|
177
|
+
|
|
178
|
+
super.handleOnDestroy();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@PluginMethod
|
|
182
|
+
public void checkPermissions(PluginCall call) {
|
|
183
|
+
JSObject result = new JSObject();
|
|
184
|
+
|
|
185
|
+
result.put("location", getLocationPermissionState());
|
|
186
|
+
result.put("backgroundLocation", getBackgroundLocationPermissionState());
|
|
187
|
+
result.put("notifications", getNotificationPermissionState());
|
|
188
|
+
|
|
189
|
+
call.resolve(result);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@PluginMethod
|
|
193
|
+
public void requestPermissions(PluginCall call) {
|
|
194
|
+
// First request basic location permissions
|
|
195
|
+
if (getLocationPermissionState() != PermissionState.GRANTED) {
|
|
196
|
+
requestPermissionForAlias("location", call, "locationPermissionCallback");
|
|
197
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
|
198
|
+
getBackgroundLocationPermissionState() != PermissionState.GRANTED) {
|
|
199
|
+
requestPermissionForAlias("backgroundLocation", call, "backgroundLocationPermissionCallback");
|
|
200
|
+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
201
|
+
getNotificationPermissionState() != PermissionState.GRANTED) {
|
|
202
|
+
requestPermissionForAlias("notifications", call, "notificationPermissionCallback");
|
|
203
|
+
} else {
|
|
204
|
+
// All permissions already granted
|
|
205
|
+
checkPermissions(call);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@PermissionCallback
|
|
210
|
+
private void locationPermissionCallback(PluginCall call) {
|
|
211
|
+
if (getLocationPermissionState() == PermissionState.GRANTED) {
|
|
212
|
+
// Continue with background location if needed
|
|
213
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
214
|
+
requestPermissionForAlias("backgroundLocation", call, "backgroundLocationPermissionCallback");
|
|
215
|
+
} else {
|
|
216
|
+
checkPermissions(call);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
call.reject("Location permission is required for this feature");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@PermissionCallback
|
|
224
|
+
private void backgroundLocationPermissionCallback(PluginCall call) {
|
|
225
|
+
// Continue with notification permission if needed
|
|
226
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
227
|
+
requestPermissionForAlias("notifications", call, "notificationPermissionCallback");
|
|
228
|
+
} else {
|
|
229
|
+
checkPermissions(call);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@PermissionCallback
|
|
234
|
+
private void notificationPermissionCallback(PluginCall call) {
|
|
235
|
+
checkPermissions(call);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Start foreground location service with notification configuration
|
|
240
|
+
*
|
|
241
|
+
* Expected format (standardized approach):
|
|
242
|
+
* {
|
|
243
|
+
* "notification": {
|
|
244
|
+
* "title": "Location Tracking Active", // REQUIRED
|
|
245
|
+
* "text": "Recording your route...", // REQUIRED
|
|
246
|
+
* "icon": "ic_location" // OPTIONAL - drawable resource name
|
|
247
|
+
* },
|
|
248
|
+
* "interval": 60000, // OPTIONAL - update interval in ms
|
|
249
|
+
* "fastestInterval": 30000, // OPTIONAL - fastest interval in ms
|
|
250
|
+
* "priority": "HIGH_ACCURACY", // OPTIONAL - location priority
|
|
251
|
+
* "distanceFilter": 0, // OPTIONAL - distance filter in meters
|
|
252
|
+
* "api": { // OPTIONAL - API service configuration
|
|
253
|
+
* "url": "https://api.example.com/locations", // REQUIRED if api block present
|
|
254
|
+
* "type": "POST", // OPTIONAL - HTTP method (default: POST)
|
|
255
|
+
* "header": { // OPTIONAL - HTTP headers
|
|
256
|
+
* "Content-Type": "application/json",
|
|
257
|
+
* "Authorization": "Bearer token"
|
|
258
|
+
* },
|
|
259
|
+
* "additionalParams": { // OPTIONAL - additional parameters to send
|
|
260
|
+
* "userId": "123",
|
|
261
|
+
* "sessionId": "abc"
|
|
262
|
+
* },
|
|
263
|
+
* "apiInterval": 5 // OPTIONAL - API call interval in minutes (default: 5)
|
|
264
|
+
* }
|
|
265
|
+
* }
|
|
266
|
+
*
|
|
267
|
+
* Error codes:
|
|
268
|
+
* - PERMISSION_DENIED: Location permission not granted
|
|
269
|
+
* - INVALID_NOTIFICATION: Missing or invalid notification configuration
|
|
270
|
+
* - INVALID_PARAMETERS: Invalid interval or priority values
|
|
271
|
+
*/
|
|
272
|
+
@PluginMethod
|
|
273
|
+
public void startForegroundLocationService(PluginCall call) {
|
|
274
|
+
if (getLocationPermissionState() != PermissionState.GRANTED) {
|
|
275
|
+
call.reject("PERMISSION_DENIED", "Location permission is required");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Validate notification configuration
|
|
280
|
+
JSObject notification = call.getObject("notification");
|
|
281
|
+
if (notification == null) {
|
|
282
|
+
call.reject("INVALID_NOTIFICATION", "notification parameter is required");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
String title = notification.getString("title");
|
|
287
|
+
String text = notification.getString("text");
|
|
288
|
+
|
|
289
|
+
if (title == null || title.trim().isEmpty()) {
|
|
290
|
+
call.reject("INVALID_NOTIFICATION", "notification.title is required and cannot be empty");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (text == null || text.trim().isEmpty()) {
|
|
295
|
+
call.reject("INVALID_NOTIFICATION", "notification.text is required and cannot be empty");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Intent serviceIntent = new Intent(getContext(), LocationForegroundService.class);
|
|
300
|
+
|
|
301
|
+
// Add notification configuration to intent
|
|
302
|
+
serviceIntent.putExtra("notificationTitle", title);
|
|
303
|
+
serviceIntent.putExtra("notificationText", text);
|
|
304
|
+
serviceIntent.putExtra("notificationIcon", notification.getString("icon")); // Can be null
|
|
305
|
+
|
|
306
|
+
// Get and validate interval values
|
|
307
|
+
long updateInterval = call.getLong("interval", 60000L); // Default 60 seconds
|
|
308
|
+
long fastestInterval = call.getLong("fastestInterval", 30000L); // Default 30 seconds
|
|
309
|
+
|
|
310
|
+
// Validate intervals
|
|
311
|
+
if (updateInterval < 1000L) {
|
|
312
|
+
call.reject("INVALID_PARAMETERS", "interval must be at least 1000ms");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (fastestInterval < 1000L) {
|
|
317
|
+
call.reject("INVALID_PARAMETERS", "fastestInterval must be at least 1000ms");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (fastestInterval > updateInterval) {
|
|
322
|
+
call.reject("INVALID_PARAMETERS", "fastestInterval cannot be greater than interval");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
String priority = call.getString("priority", "HIGH_ACCURACY");
|
|
327
|
+
if (!isValidPriority(priority)) {
|
|
328
|
+
call.reject("INVALID_PARAMETERS", "priority must be one of: HIGH_ACCURACY, BALANCED_POWER, LOW_POWER, NO_POWER");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Get and validate distance filter
|
|
333
|
+
long distanceFilter = call.getLong("distanceFilter", 0L); // Default 0 = no filter
|
|
334
|
+
if (distanceFilter < 0L) {
|
|
335
|
+
call.reject("INVALID_PARAMETERS", "distanceFilter must be 0 or greater (0 = no filter)");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
serviceIntent.putExtra("updateInterval", updateInterval);
|
|
340
|
+
serviceIntent.putExtra("fastestInterval", fastestInterval);
|
|
341
|
+
serviceIntent.putExtra("priority", priority);
|
|
342
|
+
serviceIntent.putExtra("distanceFilter", distanceFilter);
|
|
343
|
+
|
|
344
|
+
// Extract API configuration if provided
|
|
345
|
+
JSObject apiConfig = call.getObject("api");
|
|
346
|
+
if (apiConfig != null) {
|
|
347
|
+
String apiUrl = apiConfig.getString("url");
|
|
348
|
+
if (apiUrl != null && !apiUrl.trim().isEmpty()) {
|
|
349
|
+
serviceIntent.putExtra("apiUrl", apiUrl.trim());
|
|
350
|
+
serviceIntent.putExtra("apiType", apiConfig.getString("type", "POST"));
|
|
351
|
+
|
|
352
|
+
JSObject headers = apiConfig.getJSObject("header");
|
|
353
|
+
if (headers != null) {
|
|
354
|
+
serviceIntent.putExtra("apiHeaders", headers.toString());
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
JSObject additionalParams = apiConfig.getJSObject("additionalParams");
|
|
358
|
+
if (additionalParams != null) {
|
|
359
|
+
serviceIntent.putExtra("apiAdditionalParams", additionalParams.toString());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
Integer apiInterval = apiConfig.getInteger("apiInterval", 5);
|
|
363
|
+
if (apiInterval != null && apiInterval > 0) {
|
|
364
|
+
serviceIntent.putExtra("apiInterval", apiInterval);
|
|
365
|
+
} else {
|
|
366
|
+
serviceIntent.putExtra("apiInterval", 5); // Default 5 minutes
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Log.d(TAG, "API configuration added to service intent");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Start foreground service
|
|
374
|
+
ContextCompat.startForegroundService(getContext(), serviceIntent);
|
|
375
|
+
|
|
376
|
+
// Bind to service
|
|
377
|
+
getContext().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
|
|
378
|
+
|
|
379
|
+
call.resolve();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@PluginMethod
|
|
383
|
+
public void stopForegroundLocationService(PluginCall call) {
|
|
384
|
+
Intent serviceIntent = new Intent(getContext(), LocationForegroundService.class);
|
|
385
|
+
getContext().stopService(serviceIntent);
|
|
386
|
+
|
|
387
|
+
if (isServiceBound) {
|
|
388
|
+
getContext().unbindService(serviceConnection);
|
|
389
|
+
isServiceBound = false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
call.resolve();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@PluginMethod
|
|
396
|
+
public void isServiceRunning(PluginCall call) {
|
|
397
|
+
JSObject result = new JSObject();
|
|
398
|
+
result.put("isRunning", isServiceBound && locationService != null &&
|
|
399
|
+
locationService.isLocationUpdatesActive());
|
|
400
|
+
call.resolve(result);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@PluginMethod
|
|
404
|
+
public void getCurrentLocation(PluginCall call) {
|
|
405
|
+
if (getLocationPermissionState() != PermissionState.GRANTED) {
|
|
406
|
+
call.reject("Location permission required");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
implementation.getCurrentLocation(getContext(), new ForeGroundLocation.LocationCallback() {
|
|
411
|
+
@Override
|
|
412
|
+
public void onLocationResult(Location location) {
|
|
413
|
+
JSObject result = new JSObject();
|
|
414
|
+
result.put("latitude", location.getLatitude());
|
|
415
|
+
result.put("longitude", location.getLongitude());
|
|
416
|
+
result.put("accuracy", (double) location.getAccuracy());
|
|
417
|
+
result.put("timestamp", implementation.formatTimestamp(location.getTime()));
|
|
418
|
+
|
|
419
|
+
if (location.hasAltitude()) {
|
|
420
|
+
result.put("altitude", location.getAltitude());
|
|
421
|
+
}
|
|
422
|
+
if (location.hasBearing()) {
|
|
423
|
+
result.put("bearing", (double) location.getBearing());
|
|
424
|
+
}
|
|
425
|
+
if (location.hasSpeed()) {
|
|
426
|
+
result.put("speed", (double) location.getSpeed());
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
call.resolve(result);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@Override
|
|
433
|
+
public void onLocationError(String error) {
|
|
434
|
+
call.reject("Location error: " + error);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
@PluginMethod
|
|
440
|
+
public void updateLocationSettings(PluginCall call) {
|
|
441
|
+
if (!isServiceBound || locationService == null) {
|
|
442
|
+
call.reject("SERVICE_NOT_RUNNING", "Location service is not running");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Get notification configuration directly from call
|
|
447
|
+
JSObject notification = call.getObject("notification");
|
|
448
|
+
|
|
449
|
+
// Enhanced validation for notification configuration in updates
|
|
450
|
+
String title = "Location Tracking"; // Default
|
|
451
|
+
String text = "Tracking location"; // Default
|
|
452
|
+
String icon = null;
|
|
453
|
+
|
|
454
|
+
if (notification != null) {
|
|
455
|
+
// Use provided values if valid, otherwise keep defaults
|
|
456
|
+
String providedTitle = notification.getString("title");
|
|
457
|
+
String providedText = notification.getString("text");
|
|
458
|
+
|
|
459
|
+
if (providedTitle != null && !providedTitle.trim().isEmpty()) {
|
|
460
|
+
title = providedTitle;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (providedText != null && !providedText.trim().isEmpty()) {
|
|
464
|
+
text = providedText;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
icon = notification.getString("icon");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
Log.d(TAG, "Updating service configuration - Title: " + title + ", Text: " + text);
|
|
471
|
+
|
|
472
|
+
// Get interval values directly from call
|
|
473
|
+
long interval = call.getLong("interval", 60000L); // Default 60 seconds
|
|
474
|
+
long fastestInterval = call.getLong("fastestInterval", 30000L); // Default 30 seconds
|
|
475
|
+
|
|
476
|
+
// Validate intervals
|
|
477
|
+
if (interval < 1000L) {
|
|
478
|
+
call.reject("INVALID_PARAMETERS", "interval must be at least 1000ms");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (fastestInterval < 1000L) {
|
|
483
|
+
call.reject("INVALID_PARAMETERS", "fastestInterval must be at least 1000ms");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
String priorityStr = call.getString("priority", "HIGH_ACCURACY");
|
|
488
|
+
|
|
489
|
+
if (!isValidPriority(priorityStr)) {
|
|
490
|
+
call.reject("INVALID_PARAMETERS", "priority must be one of: HIGH_ACCURACY, BALANCED_POWER, LOW_POWER, NO_POWER");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
int priority = convertPriority(priorityStr);
|
|
495
|
+
|
|
496
|
+
// Get distance filter parameter
|
|
497
|
+
long distanceFilter = call.getLong("distanceFilter", 0L); // Default 0 = no filter
|
|
498
|
+
if (distanceFilter < 0L) {
|
|
499
|
+
call.reject("INVALID_PARAMETERS", "distanceFilter must be 0 or greater (0 = no filter)");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
locationService.updateServiceConfiguration(title, text, icon, interval, fastestInterval, priority, distanceFilter);
|
|
504
|
+
call.resolve();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
@PluginMethod
|
|
508
|
+
public void getApiServiceStatus(PluginCall call) {
|
|
509
|
+
if (!isServiceBound || locationService == null) {
|
|
510
|
+
call.reject("SERVICE_NOT_RUNNING", "Location service is not running");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
JSObject result = new JSObject();
|
|
515
|
+
result.put("isEnabled", locationService.isApiServiceEnabled());
|
|
516
|
+
result.put("bufferSize", locationService.getApiBufferSize());
|
|
517
|
+
result.put("isHealthy", locationService.isApiHealthy());
|
|
518
|
+
|
|
519
|
+
call.resolve(result);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@PluginMethod
|
|
523
|
+
public void clearApiBuffers(PluginCall call) {
|
|
524
|
+
if (!isServiceBound || locationService == null) {
|
|
525
|
+
call.reject("SERVICE_NOT_RUNNING", "Location service is not running");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
locationService.clearApiBuffers();
|
|
530
|
+
call.resolve();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@PluginMethod
|
|
534
|
+
public void resetApiCircuitBreaker(PluginCall call) {
|
|
535
|
+
if (!isServiceBound || locationService == null) {
|
|
536
|
+
call.reject("SERVICE_NOT_RUNNING", "Location service is not running");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
locationService.resetApiCircuitBreaker();
|
|
541
|
+
call.resolve();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Helper methods
|
|
545
|
+
|
|
546
|
+
// Helper method to debug notification configuration
|
|
547
|
+
private void logNotificationConfig(JSObject notification) {
|
|
548
|
+
if (notification == null) {
|
|
549
|
+
Log.d(TAG, "Notification config is null");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
Log.d(TAG, "Notification config received:");
|
|
554
|
+
Log.d(TAG, " - Length: " + notification.length());
|
|
555
|
+
Log.d(TAG, " - Keys: " + notification.keys());
|
|
556
|
+
|
|
557
|
+
if (notification.has("title")) {
|
|
558
|
+
Log.d(TAG, " - Title: '" + notification.getString("title") + "'");
|
|
559
|
+
} else {
|
|
560
|
+
Log.d(TAG, " - Title: NOT PROVIDED");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (notification.has("text")) {
|
|
564
|
+
Log.d(TAG, " - Text: '" + notification.getString("text") + "'");
|
|
565
|
+
} else {
|
|
566
|
+
Log.d(TAG, " - Text: NOT PROVIDED");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (notification.has("icon")) {
|
|
570
|
+
Log.d(TAG, " - Icon: '" + notification.getString("icon") + "'");
|
|
571
|
+
} else {
|
|
572
|
+
Log.d(TAG, " - Icon: NOT PROVIDED");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Helper method to safely extract and validate notification configuration
|
|
577
|
+
private boolean validateAndExtractNotification(PluginCall call) {
|
|
578
|
+
JSObject notification = call.getObject("notification");
|
|
579
|
+
|
|
580
|
+
// Debug logging for troubleshooting
|
|
581
|
+
Log.d(TAG, "validateAndExtractNotification called");
|
|
582
|
+
Log.d(TAG, "Call data received: " + call.getData().toString());
|
|
583
|
+
logNotificationConfig(notification);
|
|
584
|
+
|
|
585
|
+
// Enhanced validation for notification configuration
|
|
586
|
+
if (notification == null) {
|
|
587
|
+
call.reject("INVALID_NOTIFICATION", "Notification configuration is required. Please provide notification object with title and text properties.");
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Validate required notification properties
|
|
592
|
+
String title = notification.getString("title");
|
|
593
|
+
String text = notification.getString("text");
|
|
594
|
+
|
|
595
|
+
if (title == null || title.trim().isEmpty()) {
|
|
596
|
+
call.reject("INVALID_NOTIFICATION", "Notification title is required in notification configuration.");
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (text == null || text.trim().isEmpty()) {
|
|
601
|
+
call.reject("INVALID_NOTIFICATION", "Notification text is required in notification configuration.");
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
Log.d(TAG, "Notification configuration validated successfully - Title: " + title + ", Text: " + text);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private PermissionState getLocationPermissionState() {
|
|
610
|
+
return getPermissionState("location");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private PermissionState getBackgroundLocationPermissionState() {
|
|
614
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
615
|
+
return getPermissionState("backgroundLocation");
|
|
616
|
+
}
|
|
617
|
+
return PermissionState.GRANTED; // Not required on older versions
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private PermissionState getNotificationPermissionState() {
|
|
621
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
622
|
+
return getPermissionState("notifications");
|
|
623
|
+
}
|
|
624
|
+
return PermissionState.GRANTED; // Not required on older versions
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private int convertPriority(String priorityStr) {
|
|
628
|
+
switch (priorityStr) {
|
|
629
|
+
case "HIGH_ACCURACY":
|
|
630
|
+
return com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY;
|
|
631
|
+
case "BALANCED_POWER":
|
|
632
|
+
return com.google.android.gms.location.Priority.PRIORITY_BALANCED_POWER_ACCURACY;
|
|
633
|
+
case "LOW_POWER":
|
|
634
|
+
return com.google.android.gms.location.Priority.PRIORITY_LOW_POWER;
|
|
635
|
+
case "NO_POWER":
|
|
636
|
+
return com.google.android.gms.location.Priority.PRIORITY_PASSIVE;
|
|
637
|
+
default:
|
|
638
|
+
return com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private boolean isValidPriority(String priority) {
|
|
643
|
+
return priority != null && (
|
|
644
|
+
priority.equals("HIGH_ACCURACY") ||
|
|
645
|
+
priority.equals("BALANCED_POWER") ||
|
|
646
|
+
priority.equals("LOW_POWER") ||
|
|
647
|
+
priority.equals("NO_POWER")
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|