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.
@@ -0,0 +1,526 @@
1
+ package in.xconcepts.foreground.location;
2
+
3
+ import android.app.Notification;
4
+ import android.app.NotificationChannel;
5
+ import android.app.NotificationManager;
6
+ import android.app.PendingIntent;
7
+ import android.app.Service;
8
+ import android.content.Context;
9
+ import android.content.Intent;
10
+ import android.content.pm.PackageManager;
11
+ import android.location.Location;
12
+ import android.os.Binder;
13
+ import android.os.Build;
14
+ import android.os.IBinder;
15
+ import android.os.Looper;
16
+ import android.util.Log;
17
+
18
+ import androidx.annotation.NonNull;
19
+ import androidx.core.app.NotificationCompat;
20
+ import androidx.core.content.ContextCompat;
21
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
22
+
23
+ import com.google.android.gms.location.FusedLocationProviderClient;
24
+ import com.google.android.gms.location.LocationAvailability;
25
+ import com.google.android.gms.location.LocationCallback;
26
+ import com.google.android.gms.location.LocationRequest;
27
+ import com.google.android.gms.location.LocationResult;
28
+ import com.google.android.gms.location.LocationServices;
29
+ import com.google.android.gms.location.Priority;
30
+
31
+ import java.text.SimpleDateFormat;
32
+ import java.util.Date;
33
+ import java.util.HashMap;
34
+ import java.util.Iterator;
35
+ import java.util.Locale;
36
+ import java.util.Map;
37
+ import java.util.TimeZone;
38
+
39
+ import org.json.JSONException;
40
+ import org.json.JSONObject;
41
+
42
+ public class LocationForegroundService extends Service {
43
+ private static final String TAG = "LocationForegroundService";
44
+ private static final int NOTIFICATION_ID = 1001;
45
+ private static final String CHANNEL_ID = "location_service_channel";
46
+
47
+ public static final String ACTION_LOCATION_UPDATE = "in.xconcepts.foreground.location.LOCATION_UPDATE";
48
+ public static final String ACTION_SERVICE_STATUS = "in.xconcepts.foreground.location.SERVICE_STATUS";
49
+ public static final String EXTRA_LOCATION_DATA = "location_data";
50
+ public static final String EXTRA_SERVICE_STATUS = "service_status";
51
+ public static final String EXTRA_ERROR_MESSAGE = "error_message";
52
+
53
+ private FusedLocationProviderClient fusedLocationClient;
54
+ private LocationCallback locationCallback;
55
+ private LocationRequest locationRequest;
56
+ private NotificationManager notificationManager;
57
+ private boolean isLocationUpdatesActive = false;
58
+
59
+ // Service configuration
60
+ private String notificationTitle = "Location Tracking";
61
+ private String notificationText = "Tracking your location in the background";
62
+ private String notificationIcon = null; // Custom icon name from app
63
+ private long updateInterval = 60000; // 1 minute
64
+ private long fastestInterval = 30000; // 30 seconds
65
+ private int priority = Priority.PRIORITY_HIGH_ACCURACY;
66
+ private long distanceFilter = 0; // Distance filter in meters (0 = no filter)
67
+
68
+ // API Service Integration
69
+ private APIService apiService;
70
+ private boolean enableApiService = false;
71
+
72
+ // API Configuration
73
+ private String apiUrl;
74
+ private String apiType;
75
+ private Map<String, String> apiHeaders;
76
+ private JSONObject apiAdditionalParams;
77
+ private int apiIntervalMinutes = 5;
78
+
79
+ private final IBinder binder = new LocationServiceBinder();
80
+
81
+ public class LocationServiceBinder extends Binder {
82
+ LocationForegroundService getService() {
83
+ return LocationForegroundService.this;
84
+ }
85
+ }
86
+
87
+ @Override
88
+ public void onCreate() {
89
+ super.onCreate();
90
+ Log.d(TAG, "LocationForegroundService created");
91
+
92
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
93
+ notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
94
+ apiService = new APIService(this); // Initialize API service
95
+
96
+ createNotificationChannel();
97
+ setupLocationCallback();
98
+ }
99
+
100
+ @Override
101
+ public int onStartCommand(Intent intent, int flags, int startId) {
102
+ Log.d(TAG, "LocationForegroundService started");
103
+
104
+ if (intent != null) {
105
+ extractConfiguration(intent);
106
+ }
107
+
108
+ startForeground(NOTIFICATION_ID, createNotification());
109
+ startLocationUpdates();
110
+
111
+ // Restart service if killed by system
112
+ return START_STICKY;
113
+ }
114
+
115
+ @Override
116
+ public IBinder onBind(Intent intent) {
117
+ return binder;
118
+ }
119
+
120
+ @Override
121
+ public void onDestroy() {
122
+ Log.d(TAG, "LocationForegroundService destroyed");
123
+ stopLocationUpdates();
124
+
125
+ // Cleanup API service
126
+ if (apiService != null) {
127
+ apiService.shutdown();
128
+ }
129
+
130
+ super.onDestroy();
131
+ }
132
+
133
+ private void extractConfiguration(Intent intent) {
134
+ if (intent.hasExtra("notificationTitle")) {
135
+ notificationTitle = intent.getStringExtra("notificationTitle");
136
+ }
137
+ if (intent.hasExtra("notificationText")) {
138
+ notificationText = intent.getStringExtra("notificationText");
139
+ }
140
+ if (intent.hasExtra("notificationIcon")) {
141
+ notificationIcon = intent.getStringExtra("notificationIcon");
142
+ }
143
+ if (intent.hasExtra("updateInterval")) {
144
+ updateInterval = intent.getLongExtra("updateInterval", 60000);
145
+ }
146
+ if (intent.hasExtra("fastestInterval")) {
147
+ fastestInterval = intent.getLongExtra("fastestInterval", 30000);
148
+ }
149
+ if (intent.hasExtra("priority")) {
150
+ String priorityStr = intent.getStringExtra("priority");
151
+ priority = convertPriority(priorityStr);
152
+ }
153
+ if (intent.hasExtra("distanceFilter")) {
154
+ distanceFilter = intent.getLongExtra("distanceFilter", 0L);
155
+ Log.d(TAG, "Distance filter set to: " + distanceFilter + " meters");
156
+ }
157
+
158
+ // Extract API configuration
159
+ if (intent.hasExtra("apiUrl")) {
160
+ apiUrl = intent.getStringExtra("apiUrl");
161
+ enableApiService = apiUrl != null && !apiUrl.isEmpty();
162
+ Log.d(TAG, "API Service " + (enableApiService ? "enabled" : "disabled"));
163
+ }
164
+
165
+ if (enableApiService) {
166
+ if (intent.hasExtra("apiType")) {
167
+ apiType = intent.getStringExtra("apiType");
168
+ }
169
+ if (intent.hasExtra("apiHeaders")) {
170
+ try {
171
+ String headersJson = intent.getStringExtra("apiHeaders");
172
+ if (headersJson != null) {
173
+ JSONObject headersObj = new JSONObject(headersJson);
174
+ apiHeaders = new HashMap<>();
175
+ Iterator<String> keys = headersObj.keys();
176
+ while (keys.hasNext()) {
177
+ String key = keys.next();
178
+ apiHeaders.put(key, headersObj.getString(key));
179
+ }
180
+ }
181
+ } catch (JSONException e) {
182
+ Log.e(TAG, "Error parsing API headers", e);
183
+ }
184
+ }
185
+ if (intent.hasExtra("apiAdditionalParams")) {
186
+ try {
187
+ String paramsJson = intent.getStringExtra("apiAdditionalParams");
188
+ if (paramsJson != null && !paramsJson.isEmpty()) {
189
+ apiAdditionalParams = new JSONObject(paramsJson);
190
+ }
191
+ } catch (JSONException e) {
192
+ Log.e(TAG, "Error parsing API additional params", e);
193
+ }
194
+ }
195
+ if (intent.hasExtra("apiInterval")) {
196
+ apiIntervalMinutes = intent.getIntExtra("apiInterval", 5);
197
+ }
198
+
199
+ // Configure API service
200
+ configureApiService();
201
+ }
202
+ }
203
+
204
+ private int convertPriority(String priorityStr) {
205
+ if (priorityStr == null) return Priority.PRIORITY_HIGH_ACCURACY;
206
+
207
+ switch (priorityStr) {
208
+ case "HIGH_ACCURACY":
209
+ return Priority.PRIORITY_HIGH_ACCURACY;
210
+ case "BALANCED_POWER":
211
+ return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
212
+ case "LOW_POWER":
213
+ return Priority.PRIORITY_LOW_POWER;
214
+ case "NO_POWER":
215
+ return Priority.PRIORITY_PASSIVE;
216
+ default:
217
+ return Priority.PRIORITY_HIGH_ACCURACY;
218
+ }
219
+ }
220
+
221
+ private void configureApiService() {
222
+ if (enableApiService && apiUrl != null && !apiUrl.isEmpty()) {
223
+ apiService.configure(apiUrl, apiType, apiHeaders, apiAdditionalParams, apiIntervalMinutes);
224
+ Log.d(TAG, "API Service configured successfully");
225
+ }
226
+ }
227
+
228
+ private void createNotificationChannel() {
229
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
230
+ NotificationChannel channel = new NotificationChannel(
231
+ CHANNEL_ID,
232
+ "Location Service",
233
+ NotificationManager.IMPORTANCE_LOW
234
+ );
235
+ channel.setDescription("Continuous location tracking");
236
+ channel.setShowBadge(false);
237
+ notificationManager.createNotificationChannel(channel);
238
+ }
239
+ }
240
+
241
+ private Notification createNotification() {
242
+ Intent notificationIntent = new Intent();
243
+ PendingIntent pendingIntent = PendingIntent.getActivity(
244
+ this, 0, notificationIntent,
245
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0
246
+ );
247
+
248
+ return new NotificationCompat.Builder(this, CHANNEL_ID)
249
+ .setContentTitle(notificationTitle)
250
+ .setContentText(notificationText)
251
+ .setSmallIcon(getNotificationIcon())
252
+ .setContentIntent(pendingIntent)
253
+ .setOngoing(true)
254
+ .setPriority(NotificationCompat.PRIORITY_LOW)
255
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
256
+ .build();
257
+ }
258
+
259
+ private int getNotificationIcon() {
260
+ // Try custom icon first if provided
261
+ if (notificationIcon != null && !notificationIcon.isEmpty()) {
262
+ int customIconResId = getResources().getIdentifier(
263
+ notificationIcon,
264
+ "drawable",
265
+ getPackageName()
266
+ );
267
+
268
+ if (customIconResId != 0) {
269
+ Log.d(TAG, "Using custom notification icon: " + notificationIcon);
270
+ return customIconResId;
271
+ } else {
272
+ Log.w(TAG, "Custom icon not found: " + notificationIcon + ", falling back to app icon");
273
+ }
274
+ }
275
+
276
+ // Try to get application icon
277
+ try {
278
+ android.content.pm.ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
279
+ getPackageName(),
280
+ android.content.pm.PackageManager.GET_META_DATA
281
+ );
282
+
283
+ if (appInfo.icon != 0) {
284
+ Log.d(TAG, "Using application icon for notification");
285
+ return appInfo.icon;
286
+ }
287
+ } catch (android.content.pm.PackageManager.NameNotFoundException e) {
288
+ Log.e(TAG, "Could not get application info", e);
289
+ }
290
+
291
+ // Final fallback to system location icon
292
+ Log.d(TAG, "Using system default location icon");
293
+ return android.R.drawable.ic_menu_mylocation;
294
+ }
295
+
296
+ private void setupLocationCallback() {
297
+ locationCallback = new LocationCallback() {
298
+ @Override
299
+ public void onLocationResult(@NonNull LocationResult locationResult) {
300
+ for (Location location : locationResult.getLocations()) {
301
+ processLocationUpdate(location);
302
+ }
303
+ }
304
+
305
+ @Override
306
+ public void onLocationAvailability(@NonNull LocationAvailability locationAvailability) {
307
+ Log.d(TAG, "Location availability: " + locationAvailability.isLocationAvailable());
308
+ if (!locationAvailability.isLocationAvailable()) {
309
+ broadcastServiceStatus(false, "Location not available");
310
+ }
311
+ }
312
+ };
313
+ }
314
+
315
+ private void startLocationUpdates() {
316
+ if (!hasLocationPermissions()) {
317
+ Log.e(TAG, "Location permissions not granted");
318
+ broadcastServiceStatus(false, "Location permissions not granted");
319
+ return;
320
+ }
321
+
322
+ createLocationRequest();
323
+
324
+ try {
325
+ fusedLocationClient.requestLocationUpdates(
326
+ locationRequest,
327
+ locationCallback,
328
+ Looper.getMainLooper()
329
+ );
330
+ isLocationUpdatesActive = true;
331
+
332
+ // Start API service if configured
333
+ if (enableApiService) {
334
+ apiService.startApiService();
335
+ Log.d(TAG, "API Service started");
336
+ }
337
+
338
+ broadcastServiceStatus(true, null);
339
+ Log.d(TAG, "Location updates started");
340
+ } catch (SecurityException e) {
341
+ Log.e(TAG, "Security exception when requesting location updates", e);
342
+ broadcastServiceStatus(false, "Security exception: " + e.getMessage());
343
+ }
344
+ }
345
+
346
+ private void stopLocationUpdates() {
347
+ if (fusedLocationClient != null && locationCallback != null) {
348
+ fusedLocationClient.removeLocationUpdates(locationCallback);
349
+ isLocationUpdatesActive = false;
350
+
351
+ // Stop API service
352
+ if (apiService != null) {
353
+ apiService.stopApiService();
354
+ Log.d(TAG, "API Service stopped");
355
+ }
356
+
357
+ broadcastServiceStatus(false, null);
358
+ Log.d(TAG, "Location updates stopped");
359
+ }
360
+ }
361
+
362
+ private void createLocationRequest() {
363
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
364
+ // Use new Builder API (API 31+)
365
+ LocationRequest.Builder builder = new LocationRequest.Builder(priority, updateInterval)
366
+ .setWaitForAccurateLocation(false)
367
+ .setMinUpdateIntervalMillis(fastestInterval)
368
+ .setMaxUpdateDelayMillis(updateInterval * 2);
369
+
370
+ // Add distance filter if specified
371
+ if (distanceFilter > 0) {
372
+ builder.setMinUpdateDistanceMeters((float) distanceFilter);
373
+ Log.d(TAG, "Distance filter applied: " + distanceFilter + " meters");
374
+ }
375
+
376
+ locationRequest = builder.build();
377
+ } else {
378
+ // Use legacy API (API 23+)
379
+ locationRequest = LocationRequest.create()
380
+ .setPriority(priority)
381
+ .setInterval(updateInterval)
382
+ .setFastestInterval(fastestInterval)
383
+ .setMaxWaitTime(updateInterval * 2);
384
+
385
+ // Add distance filter for legacy API
386
+ if (distanceFilter > 0) {
387
+ locationRequest.setSmallestDisplacement((float) distanceFilter);
388
+ Log.d(TAG, "Distance filter applied (legacy): " + distanceFilter + " meters");
389
+ }
390
+ }
391
+ }
392
+
393
+ private boolean hasLocationPermissions() {
394
+ return ContextCompat.checkSelfPermission(this,
395
+ android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
396
+ }
397
+
398
+ private void processLocationUpdate(Location location) {
399
+ Log.d(TAG, "Location update: " + location.getLatitude() + ", " + location.getLongitude());
400
+
401
+ // Create ISO 8601 timestamp
402
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
403
+ sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
404
+ String timestamp = sdf.format(new Date(location.getTime()));
405
+
406
+ // ALWAYS broadcast to Ionic app (regardless of API configuration)
407
+ Intent intent = new Intent(ACTION_LOCATION_UPDATE);
408
+ intent.putExtra("latitude", location.getLatitude());
409
+ intent.putExtra("longitude", location.getLongitude());
410
+ intent.putExtra("accuracy", (double) location.getAccuracy());
411
+ intent.putExtra("timestamp", timestamp);
412
+
413
+ if (location.hasAltitude()) {
414
+ intent.putExtra("altitude", location.getAltitude());
415
+ }
416
+ if (location.hasBearing()) {
417
+ intent.putExtra("bearing", (double) location.getBearing());
418
+ }
419
+ if (location.hasSpeed()) {
420
+ intent.putExtra("speed", (double) location.getSpeed());
421
+ }
422
+
423
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
424
+
425
+ // ADDITIONALLY send to API service if enabled
426
+ if (enableApiService && apiService != null) {
427
+ try {
428
+ JSONObject locationData = new JSONObject();
429
+ locationData.put("latitude", location.getLatitude());
430
+ locationData.put("longitude", location.getLongitude());
431
+ locationData.put("accuracy", (double) location.getAccuracy());
432
+ locationData.put("timestamp", timestamp);
433
+
434
+ if (location.hasAltitude()) {
435
+ locationData.put("altitude", location.getAltitude());
436
+ }
437
+ if (location.hasBearing()) {
438
+ locationData.put("bearing", (double) location.getBearing());
439
+ }
440
+ if (location.hasSpeed()) {
441
+ locationData.put("speed", (double) location.getSpeed());
442
+ }
443
+
444
+ apiService.addLocationData(locationData);
445
+ } catch (JSONException e) {
446
+ Log.e(TAG, "Error creating location data JSON for API", e);
447
+ }
448
+ }
449
+
450
+ // Update notification with current location
451
+ updateNotificationWithLocation(location);
452
+ }
453
+
454
+ private void updateNotificationWithLocation(Location location) {
455
+ // String updatedText = String.format(Locale.US,
456
+ // "Lat: %.6f, Lng: %.6f",
457
+ // location.getLatitude(),
458
+ // location.getLongitude()
459
+ // );
460
+
461
+ Notification updatedNotification = new NotificationCompat.Builder(this, CHANNEL_ID)
462
+ .setContentTitle(notificationTitle)
463
+ .setContentText(notificationText)
464
+ .setSmallIcon(getNotificationIcon())
465
+ .setOngoing(true)
466
+ .setPriority(NotificationCompat.PRIORITY_LOW)
467
+ .build();
468
+
469
+ notificationManager.notify(NOTIFICATION_ID, updatedNotification);
470
+ }
471
+
472
+ private void broadcastServiceStatus(boolean isRunning, String error) {
473
+ Intent intent = new Intent(ACTION_SERVICE_STATUS);
474
+ intent.putExtra(EXTRA_SERVICE_STATUS, isRunning);
475
+ if (error != null) {
476
+ intent.putExtra(EXTRA_ERROR_MESSAGE, error);
477
+ }
478
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
479
+ }
480
+
481
+ // Public methods for service control
482
+ public boolean isLocationUpdatesActive() {
483
+ return isLocationUpdatesActive;
484
+ }
485
+
486
+ public int getApiBufferSize() {
487
+ return apiService != null ? apiService.getBufferSize() : 0;
488
+ }
489
+
490
+ public void clearApiBuffers() {
491
+ if (apiService != null) {
492
+ apiService.clearBuffers();
493
+ }
494
+ }
495
+
496
+ public boolean isApiServiceEnabled() {
497
+ return enableApiService;
498
+ }
499
+
500
+ public boolean isApiHealthy() {
501
+ return apiService != null ? apiService.isApiHealthy() : false;
502
+ }
503
+
504
+ public void resetApiCircuitBreaker() {
505
+ if (apiService != null) {
506
+ apiService.resetCircuitBreaker();
507
+ }
508
+ }
509
+
510
+ public void updateServiceConfiguration(String title, String text, String icon, long interval, long fastest, int priority, long distanceFilter) {
511
+ this.notificationTitle = title;
512
+ this.notificationText = text;
513
+ this.notificationIcon = icon;
514
+ this.updateInterval = interval;
515
+ this.fastestInterval = fastest;
516
+ this.priority = priority;
517
+ this.distanceFilter = distanceFilter;
518
+
519
+ // Restart location updates with new configuration
520
+ stopLocationUpdates();
521
+ startLocationUpdates();
522
+
523
+ // Update notification
524
+ notificationManager.notify(NOTIFICATION_ID, createNotification());
525
+ }
526
+ }
File without changes