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,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
+ }