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,74 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
|
|
4
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
|
|
5
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
repositories {
|
|
10
|
+
google()
|
|
11
|
+
mavenCentral()
|
|
12
|
+
}
|
|
13
|
+
dependencies {
|
|
14
|
+
classpath 'com.android.tools.build:gradle:8.7.2'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
apply plugin: 'com.android.library'
|
|
19
|
+
|
|
20
|
+
android {
|
|
21
|
+
namespace "in.xconcepts.foreground.location"
|
|
22
|
+
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
|
|
23
|
+
defaultConfig {
|
|
24
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
25
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
|
|
26
|
+
versionCode 1
|
|
27
|
+
versionName "1.0"
|
|
28
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
29
|
+
}
|
|
30
|
+
buildTypes {
|
|
31
|
+
release {
|
|
32
|
+
minifyEnabled false
|
|
33
|
+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lintOptions {
|
|
37
|
+
abortOnError false
|
|
38
|
+
}
|
|
39
|
+
compileOptions {
|
|
40
|
+
sourceCompatibility JavaVersion.VERSION_21
|
|
41
|
+
targetCompatibility JavaVersion.VERSION_21
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
repositories {
|
|
46
|
+
google()
|
|
47
|
+
mavenCentral()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
dependencies {
|
|
52
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
53
|
+
implementation project(':capacitor-android')
|
|
54
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
55
|
+
|
|
56
|
+
// Google Play Services Location - pin specific version for stability
|
|
57
|
+
implementation 'com.google.android.gms:play-services-location:21.0.1'
|
|
58
|
+
|
|
59
|
+
// LocalBroadcastManager for communication between service and plugin
|
|
60
|
+
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
|
61
|
+
|
|
62
|
+
testImplementation "junit:junit:$junitVersion"
|
|
63
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
64
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add version conflict resolution
|
|
68
|
+
configurations.all {
|
|
69
|
+
resolutionStrategy {
|
|
70
|
+
// Force specific versions to avoid conflicts
|
|
71
|
+
force 'androidx.core:core:1.10.1'
|
|
72
|
+
force 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
|
|
3
|
+
<!-- Location permissions -->
|
|
4
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
5
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
6
|
+
|
|
7
|
+
<!-- Foreground service permissions -->
|
|
8
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
9
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
|
10
|
+
|
|
11
|
+
<!-- Notification permission for Android 13+ -->
|
|
12
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
13
|
+
|
|
14
|
+
<!-- Background location permission for Android 10+ -->
|
|
15
|
+
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
16
|
+
|
|
17
|
+
<!-- Internet access for Google Play Services -->
|
|
18
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
19
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
20
|
+
|
|
21
|
+
<application>
|
|
22
|
+
<!-- Foreground Location Service -->
|
|
23
|
+
<service
|
|
24
|
+
android:name="in.xconcepts.foreground.location.LocationForegroundService"
|
|
25
|
+
android:enabled="true"
|
|
26
|
+
android:exported="false"
|
|
27
|
+
android:foregroundServiceType="location" />
|
|
28
|
+
</application>
|
|
29
|
+
|
|
30
|
+
</manifest>
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
package in.xconcepts.foreground.location;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.os.Handler;
|
|
5
|
+
import android.os.Looper;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
|
|
8
|
+
import org.json.JSONArray;
|
|
9
|
+
import org.json.JSONException;
|
|
10
|
+
import org.json.JSONObject;
|
|
11
|
+
|
|
12
|
+
import java.io.BufferedReader;
|
|
13
|
+
import java.io.IOException;
|
|
14
|
+
import java.io.InputStreamReader;
|
|
15
|
+
import java.io.OutputStream;
|
|
16
|
+
import java.net.HttpURLConnection;
|
|
17
|
+
import java.net.URL;
|
|
18
|
+
import java.util.ArrayList;
|
|
19
|
+
import java.util.HashMap;
|
|
20
|
+
import java.util.Iterator;
|
|
21
|
+
import java.util.List;
|
|
22
|
+
import java.util.Map;
|
|
23
|
+
import java.util.concurrent.ExecutorService;
|
|
24
|
+
import java.util.concurrent.Executors;
|
|
25
|
+
|
|
26
|
+
public class APIService {
|
|
27
|
+
private static final String TAG = "APIService";
|
|
28
|
+
private static final int MAX_RETRY_ATTEMPTS = 3;
|
|
29
|
+
private static final long RETRY_DELAY_MS = 5000; // 5 seconds
|
|
30
|
+
private static final int MAX_FAILED_BUFFER_SIZE = 1000; // Prevent memory issues
|
|
31
|
+
private static final long CIRCUIT_BREAKER_TIMEOUT = 300000; // 5 minutes
|
|
32
|
+
private static final int CIRCUIT_BREAKER_FAILURE_THRESHOLD = 5;
|
|
33
|
+
private static final int BATCH_SIZE = 100; // Maximum points per API call
|
|
34
|
+
|
|
35
|
+
private Context context;
|
|
36
|
+
private ExecutorService executorService;
|
|
37
|
+
private Handler mainHandler;
|
|
38
|
+
|
|
39
|
+
// API Configuration
|
|
40
|
+
private String apiUrl;
|
|
41
|
+
private String apiType; // GET, POST, PUT, PATCH
|
|
42
|
+
private Map<String, String> headers;
|
|
43
|
+
private JSONObject additionalParams;
|
|
44
|
+
private long apiIntervalMs;
|
|
45
|
+
|
|
46
|
+
// Data Management
|
|
47
|
+
private List<JSONObject> locationDataBuffer;
|
|
48
|
+
private List<JSONObject> failedDataBuffer;
|
|
49
|
+
private Runnable apiTask;
|
|
50
|
+
private boolean isRunning = false;
|
|
51
|
+
|
|
52
|
+
// Circuit breaker pattern
|
|
53
|
+
private boolean circuitBreakerOpen = false;
|
|
54
|
+
private long circuitBreakerOpenTime = 0;
|
|
55
|
+
private int consecutiveFailures = 0;
|
|
56
|
+
|
|
57
|
+
public APIService(Context context) {
|
|
58
|
+
this.context = context;
|
|
59
|
+
this.executorService = Executors.newSingleThreadExecutor();
|
|
60
|
+
this.mainHandler = new Handler(Looper.getMainLooper());
|
|
61
|
+
this.locationDataBuffer = new ArrayList<>();
|
|
62
|
+
this.failedDataBuffer = new ArrayList<>();
|
|
63
|
+
this.headers = new HashMap<>();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public void configure(String url, String type, Map<String, String> headers,
|
|
67
|
+
JSONObject additionalParams, long intervalMinutes) {
|
|
68
|
+
this.apiUrl = url;
|
|
69
|
+
this.apiType = type != null ? type.toUpperCase() : "POST";
|
|
70
|
+
this.headers = headers != null ? new HashMap<>(headers) : new HashMap<>();
|
|
71
|
+
this.additionalParams = additionalParams;
|
|
72
|
+
this.apiIntervalMs = intervalMinutes * 60 * 1000; // Convert minutes to milliseconds
|
|
73
|
+
|
|
74
|
+
Log.d(TAG, "API Service configured - URL: " + url + ", Interval: " + intervalMinutes + " minutes");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public void startApiService() {
|
|
78
|
+
if (isRunning || apiUrl == null || apiUrl.isEmpty()) {
|
|
79
|
+
Log.w(TAG, "API Service already running or not configured properly");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isRunning = true;
|
|
84
|
+
scheduleApiCall();
|
|
85
|
+
Log.d(TAG, "API Service started");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public void stopApiService() {
|
|
89
|
+
isRunning = false;
|
|
90
|
+
if (apiTask != null) {
|
|
91
|
+
mainHandler.removeCallbacks(apiTask);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Send remaining data before stopping
|
|
95
|
+
if (!locationDataBuffer.isEmpty() || !failedDataBuffer.isEmpty()) {
|
|
96
|
+
executorService.execute(this::sendLocationData);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Log.d(TAG, "API Service stopped");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public synchronized void addLocationData(JSONObject locationData) {
|
|
103
|
+
locationDataBuffer.add(locationData);
|
|
104
|
+
Log.d(TAG, "Location data added to buffer. Buffer size: " + locationDataBuffer.size());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private void scheduleApiCall() {
|
|
108
|
+
if (!isRunning) return;
|
|
109
|
+
|
|
110
|
+
apiTask = new Runnable() {
|
|
111
|
+
@Override
|
|
112
|
+
public void run() {
|
|
113
|
+
if (isRunning) {
|
|
114
|
+
executorService.execute(() -> {
|
|
115
|
+
sendLocationData();
|
|
116
|
+
if (isRunning) {
|
|
117
|
+
mainHandler.postDelayed(this, apiIntervalMs);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
mainHandler.postDelayed(apiTask, apiIntervalMs);
|
|
125
|
+
Log.d(TAG, "Next API call scheduled in " + (apiIntervalMs / 1000) + " seconds");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private void sendLocationData() {
|
|
129
|
+
List<JSONObject> dataToSend = new ArrayList<>();
|
|
130
|
+
|
|
131
|
+
synchronized (this) {
|
|
132
|
+
// Limit failed buffer size to prevent memory issues
|
|
133
|
+
if (failedDataBuffer.size() > MAX_FAILED_BUFFER_SIZE) {
|
|
134
|
+
int removeCount = failedDataBuffer.size() - MAX_FAILED_BUFFER_SIZE;
|
|
135
|
+
Log.w(TAG, "Failed buffer too large, removing " + removeCount + " oldest entries");
|
|
136
|
+
failedDataBuffer.subList(0, removeCount).clear();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add failed data from previous attempts FIRST
|
|
140
|
+
dataToSend.addAll(failedDataBuffer);
|
|
141
|
+
failedDataBuffer.clear();
|
|
142
|
+
|
|
143
|
+
// Add current buffer data
|
|
144
|
+
dataToSend.addAll(locationDataBuffer);
|
|
145
|
+
locationDataBuffer.clear();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dataToSend.isEmpty()) {
|
|
149
|
+
Log.d(TAG, "No location data to send");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Log.d(TAG, "Attempting to send " + dataToSend.size() + " location points");
|
|
154
|
+
|
|
155
|
+
// Split large payloads if necessary
|
|
156
|
+
if (dataToSend.size() > BATCH_SIZE) {
|
|
157
|
+
sendDataInBatches(dataToSend);
|
|
158
|
+
} else {
|
|
159
|
+
JSONObject requestBody = createRequestBody(dataToSend);
|
|
160
|
+
boolean success = sendApiRequest(requestBody, 0);
|
|
161
|
+
handleSendResult(success, dataToSend);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private void sendDataInBatches(List<JSONObject> allData) {
|
|
166
|
+
List<JSONObject> failedBatch = new ArrayList<>();
|
|
167
|
+
|
|
168
|
+
for (int i = 0; i < allData.size(); i += BATCH_SIZE) {
|
|
169
|
+
int endIndex = Math.min(i + BATCH_SIZE, allData.size());
|
|
170
|
+
List<JSONObject> batch = allData.subList(i, endIndex);
|
|
171
|
+
|
|
172
|
+
JSONObject requestBody = createRequestBody(batch);
|
|
173
|
+
boolean success = sendApiRequest(requestBody, 0);
|
|
174
|
+
|
|
175
|
+
if (!success) {
|
|
176
|
+
failedBatch.addAll(batch);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
handleSendResult(failedBatch.isEmpty(), failedBatch);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private void handleSendResult(boolean success, List<JSONObject> data) {
|
|
184
|
+
if (!success && !data.isEmpty()) {
|
|
185
|
+
synchronized (this) {
|
|
186
|
+
failedDataBuffer.addAll(data);
|
|
187
|
+
}
|
|
188
|
+
Log.w(TAG, "API call failed, " + data.size() + " points added to retry buffer");
|
|
189
|
+
} else if (success && !data.isEmpty()) {
|
|
190
|
+
Log.d(TAG, "Successfully sent " + data.size() + " location points");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private JSONObject createRequestBody(List<JSONObject> locationData) {
|
|
195
|
+
JSONObject requestBody = new JSONObject();
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
JSONArray locationArray = new JSONArray();
|
|
199
|
+
for (JSONObject location : locationData) {
|
|
200
|
+
locationArray.put(location);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
requestBody.put("locationData", locationArray);
|
|
204
|
+
|
|
205
|
+
if (additionalParams != null) {
|
|
206
|
+
requestBody.put("additionalParams", additionalParams);
|
|
207
|
+
} else {
|
|
208
|
+
requestBody.put("additionalParams", JSONObject.NULL);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
} catch (JSONException e) {
|
|
212
|
+
Log.e(TAG, "Error creating request body", e);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return requestBody;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private boolean sendApiRequest(JSONObject requestBody, int retryAttempt) {
|
|
219
|
+
// Check circuit breaker
|
|
220
|
+
if (circuitBreakerOpen) {
|
|
221
|
+
long currentTime = System.currentTimeMillis();
|
|
222
|
+
if (currentTime - circuitBreakerOpenTime < CIRCUIT_BREAKER_TIMEOUT) {
|
|
223
|
+
Log.d(TAG, "Circuit breaker is open, skipping API call");
|
|
224
|
+
return false;
|
|
225
|
+
} else {
|
|
226
|
+
// Try to close circuit breaker
|
|
227
|
+
circuitBreakerOpen = false;
|
|
228
|
+
consecutiveFailures = 0;
|
|
229
|
+
Log.d(TAG, "Circuit breaker timeout expired, attempting to close");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (retryAttempt >= MAX_RETRY_ATTEMPTS) {
|
|
234
|
+
Log.e(TAG, "Max retry attempts reached");
|
|
235
|
+
handleConsecutiveFailure();
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
HttpURLConnection connection = null;
|
|
240
|
+
try {
|
|
241
|
+
URL url = new URL(apiUrl);
|
|
242
|
+
connection = (HttpURLConnection) url.openConnection();
|
|
243
|
+
|
|
244
|
+
// Enhanced timeout settings
|
|
245
|
+
connection.setRequestMethod(apiType);
|
|
246
|
+
connection.setDoOutput(true);
|
|
247
|
+
connection.setConnectTimeout(30000);
|
|
248
|
+
connection.setReadTimeout(60000);
|
|
249
|
+
connection.setUseCaches(false);
|
|
250
|
+
|
|
251
|
+
// Set headers
|
|
252
|
+
connection.setRequestProperty("Content-Type", "application/json");
|
|
253
|
+
connection.setRequestProperty("Accept", "application/json");
|
|
254
|
+
connection.setRequestProperty("User-Agent", "ForegroundLocationPlugin/1.0");
|
|
255
|
+
|
|
256
|
+
for (Map.Entry<String, String> header : headers.entrySet()) {
|
|
257
|
+
connection.setRequestProperty(header.getKey(), header.getValue());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Send request body
|
|
261
|
+
if (!"GET".equals(apiType)) {
|
|
262
|
+
try (OutputStream os = connection.getOutputStream()) {
|
|
263
|
+
byte[] input = requestBody.toString().getBytes("utf-8");
|
|
264
|
+
os.write(input, 0, input.length);
|
|
265
|
+
os.flush();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
int responseCode = connection.getResponseCode();
|
|
270
|
+
Log.d(TAG, "API Response Code: " + responseCode + " for " +
|
|
271
|
+
requestBody.optJSONArray("locationData").length() + " points");
|
|
272
|
+
|
|
273
|
+
if (responseCode >= 200 && responseCode < 300) {
|
|
274
|
+
// Success - reset failure counter
|
|
275
|
+
consecutiveFailures = 0;
|
|
276
|
+
if (circuitBreakerOpen) {
|
|
277
|
+
circuitBreakerOpen = false;
|
|
278
|
+
Log.d(TAG, "Circuit breaker closed after successful request");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
String response = readResponse(connection);
|
|
282
|
+
Log.d(TAG, "API Success Response: " + response);
|
|
283
|
+
return true;
|
|
284
|
+
} else {
|
|
285
|
+
// Handle different error codes
|
|
286
|
+
String errorResponse = readErrorResponse(connection);
|
|
287
|
+
Log.e(TAG, "API Error Response (" + responseCode + "): " + errorResponse);
|
|
288
|
+
|
|
289
|
+
return handleHttpError(responseCode, requestBody, retryAttempt);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
} catch (IOException e) {
|
|
293
|
+
Log.e(TAG, "Network error during API call (attempt " + (retryAttempt + 1) + ")", e);
|
|
294
|
+
return handleNetworkError(requestBody, retryAttempt);
|
|
295
|
+
} finally {
|
|
296
|
+
if (connection != null) {
|
|
297
|
+
connection.disconnect();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private boolean handleHttpError(int responseCode, JSONObject requestBody, int retryAttempt) {
|
|
303
|
+
// Determine if we should retry based on error code
|
|
304
|
+
boolean shouldRetry = false;
|
|
305
|
+
|
|
306
|
+
switch (responseCode) {
|
|
307
|
+
case 408: // Request Timeout
|
|
308
|
+
case 429: // Too Many Requests
|
|
309
|
+
case 500: // Internal Server Error
|
|
310
|
+
case 502: // Bad Gateway
|
|
311
|
+
case 503: // Service Unavailable
|
|
312
|
+
case 504: // Gateway Timeout
|
|
313
|
+
shouldRetry = true;
|
|
314
|
+
break;
|
|
315
|
+
case 401: // Unauthorized
|
|
316
|
+
case 403: // Forbidden
|
|
317
|
+
Log.e(TAG, "Authentication/Authorization error - check API credentials");
|
|
318
|
+
handleConsecutiveFailure();
|
|
319
|
+
return false;
|
|
320
|
+
case 400: // Bad Request
|
|
321
|
+
case 422: // Unprocessable Entity
|
|
322
|
+
Log.e(TAG, "Client error - request format issue");
|
|
323
|
+
return false;
|
|
324
|
+
default:
|
|
325
|
+
if (responseCode >= 500) {
|
|
326
|
+
shouldRetry = true;
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (shouldRetry && retryAttempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
332
|
+
long delay = calculateRetryDelay(retryAttempt, responseCode);
|
|
333
|
+
Log.d(TAG, "Retrying API call in " + delay + "ms (attempt " + (retryAttempt + 1) + ")");
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
Thread.sleep(delay);
|
|
337
|
+
} catch (InterruptedException e) {
|
|
338
|
+
Thread.currentThread().interrupt();
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return sendApiRequest(requestBody, retryAttempt + 1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
handleConsecutiveFailure();
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private boolean handleNetworkError(JSONObject requestBody, int retryAttempt) {
|
|
350
|
+
if (retryAttempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
351
|
+
long delay = calculateRetryDelay(retryAttempt, 0);
|
|
352
|
+
Log.d(TAG, "Retrying API call after network error in " + delay + "ms");
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
Thread.sleep(delay);
|
|
356
|
+
} catch (InterruptedException ie) {
|
|
357
|
+
Thread.currentThread().interrupt();
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return sendApiRequest(requestBody, retryAttempt + 1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
handleConsecutiveFailure();
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private long calculateRetryDelay(int retryAttempt, int responseCode) {
|
|
369
|
+
// Base delay with exponential backoff
|
|
370
|
+
long baseDelay = RETRY_DELAY_MS * (long) Math.pow(2, retryAttempt);
|
|
371
|
+
|
|
372
|
+
// Add jitter to prevent thundering herd
|
|
373
|
+
long jitter = (long) (Math.random() * 1000);
|
|
374
|
+
|
|
375
|
+
// Special handling for rate limiting
|
|
376
|
+
if (responseCode == 429) {
|
|
377
|
+
baseDelay = Math.max(baseDelay, 60000); // Minimum 1 minute for rate limits
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return baseDelay + jitter;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private void handleConsecutiveFailure() {
|
|
384
|
+
consecutiveFailures++;
|
|
385
|
+
Log.w(TAG, "Consecutive failures: " + consecutiveFailures);
|
|
386
|
+
|
|
387
|
+
if (consecutiveFailures >= CIRCUIT_BREAKER_FAILURE_THRESHOLD) {
|
|
388
|
+
circuitBreakerOpen = true;
|
|
389
|
+
circuitBreakerOpenTime = System.currentTimeMillis();
|
|
390
|
+
Log.w(TAG, "Circuit breaker opened due to consecutive failures");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private String readResponse(HttpURLConnection connection) throws IOException {
|
|
395
|
+
try (BufferedReader reader = new BufferedReader(
|
|
396
|
+
new InputStreamReader(connection.getInputStream()))) {
|
|
397
|
+
StringBuilder response = new StringBuilder();
|
|
398
|
+
String line;
|
|
399
|
+
while ((line = reader.readLine()) != null) {
|
|
400
|
+
response.append(line);
|
|
401
|
+
}
|
|
402
|
+
return response.toString();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private String readErrorResponse(HttpURLConnection connection) {
|
|
407
|
+
try (BufferedReader reader = new BufferedReader(
|
|
408
|
+
new InputStreamReader(connection.getErrorStream()))) {
|
|
409
|
+
StringBuilder response = new StringBuilder();
|
|
410
|
+
String line;
|
|
411
|
+
while ((line = reader.readLine()) != null) {
|
|
412
|
+
response.append(line);
|
|
413
|
+
}
|
|
414
|
+
return response.toString();
|
|
415
|
+
} catch (IOException e) {
|
|
416
|
+
return "Unable to read error response";
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public int getBufferSize() {
|
|
421
|
+
return locationDataBuffer.size() + failedDataBuffer.size();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
public void clearBuffers() {
|
|
425
|
+
synchronized (this) {
|
|
426
|
+
locationDataBuffer.clear();
|
|
427
|
+
failedDataBuffer.clear();
|
|
428
|
+
}
|
|
429
|
+
Log.d(TAG, "All buffers cleared");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public boolean isApiHealthy() {
|
|
433
|
+
return !circuitBreakerOpen && consecutiveFailures < CIRCUIT_BREAKER_FAILURE_THRESHOLD;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
public void resetCircuitBreaker() {
|
|
437
|
+
circuitBreakerOpen = false;
|
|
438
|
+
consecutiveFailures = 0;
|
|
439
|
+
circuitBreakerOpenTime = 0;
|
|
440
|
+
Log.d(TAG, "Circuit breaker manually reset");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
public void shutdown() {
|
|
444
|
+
stopApiService();
|
|
445
|
+
if (executorService != null && !executorService.isShutdown()) {
|
|
446
|
+
executorService.shutdown();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package in.xconcepts.foreground.location;
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.location.Location;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
|
|
8
|
+
import com.google.android.gms.location.FusedLocationProviderClient;
|
|
9
|
+
import com.google.android.gms.location.LocationServices;
|
|
10
|
+
import com.google.android.gms.location.Priority;
|
|
11
|
+
import com.google.android.gms.tasks.OnFailureListener;
|
|
12
|
+
import com.google.android.gms.tasks.OnSuccessListener;
|
|
13
|
+
|
|
14
|
+
import java.text.SimpleDateFormat;
|
|
15
|
+
import java.util.Date;
|
|
16
|
+
import java.util.Locale;
|
|
17
|
+
import java.util.TimeZone;
|
|
18
|
+
|
|
19
|
+
public class ForeGroundLocation {
|
|
20
|
+
private static final String TAG = "ForeGroundLocation";
|
|
21
|
+
|
|
22
|
+
public interface LocationCallback {
|
|
23
|
+
void onLocationResult(Location location);
|
|
24
|
+
void onLocationError(String error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@SuppressLint("MissingPermission")
|
|
28
|
+
public void getCurrentLocation(Context context, LocationCallback callback) {
|
|
29
|
+
FusedLocationProviderClient fusedLocationClient =
|
|
30
|
+
LocationServices.getFusedLocationProviderClient(context);
|
|
31
|
+
|
|
32
|
+
fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
|
|
33
|
+
.addOnSuccessListener(new OnSuccessListener<Location>() {
|
|
34
|
+
@Override
|
|
35
|
+
public void onSuccess(Location location) {
|
|
36
|
+
if (location != null) {
|
|
37
|
+
Log.d(TAG, "Location retrieved: " + location.getLatitude() + ", " + location.getLongitude());
|
|
38
|
+
callback.onLocationResult(location);
|
|
39
|
+
} else {
|
|
40
|
+
callback.onLocationError("Location is null");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.addOnFailureListener(new OnFailureListener() {
|
|
45
|
+
@Override
|
|
46
|
+
public void onFailure(Exception e) {
|
|
47
|
+
Log.e(TAG, "Failed to get location", e);
|
|
48
|
+
callback.onLocationError(e.getMessage());
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public String formatTimestamp(long timeMillis) {
|
|
54
|
+
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
|
55
|
+
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
56
|
+
return sdf.format(new Date(timeMillis));
|
|
57
|
+
}
|
|
58
|
+
}
|