@josuelmm/capacitor-background-geolocation 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/JosuelmmCapacitorBackgroundGeolocation.podspec +34 -0
- package/LICENSE +17 -0
- package/NOTICE.md +32 -0
- package/Package.swift +45 -0
- package/README.md +402 -0
- package/android/build.gradle +79 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +83 -0
- package/android/src/main/java/com/evgenii/jsevaluator/HandlerWrapper.java +18 -0
- package/android/src/main/java/com/evgenii/jsevaluator/JavaScriptInterface.java +22 -0
- package/android/src/main/java/com/evgenii/jsevaluator/JsEvaluator.java +133 -0
- package/android/src/main/java/com/evgenii/jsevaluator/JsFunctionCallFormatter.java +37 -0
- package/android/src/main/java/com/evgenii/jsevaluator/WebViewWrapper.java +71 -0
- package/android/src/main/java/com/evgenii/jsevaluator/interfaces/CallJavaResultInterface.java +8 -0
- package/android/src/main/java/com/evgenii/jsevaluator/interfaces/HandlerWrapperInterface.java +5 -0
- package/android/src/main/java/com/evgenii/jsevaluator/interfaces/JsCallback.java +10 -0
- package/android/src/main/java/com/evgenii/jsevaluator/interfaces/JsEvaluatorInterface.java +18 -0
- package/android/src/main/java/com/evgenii/jsevaluator/interfaces/WebViewWrapperInterface.java +14 -0
- package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/BackgroundGeolocationPlugin.java +898 -0
- package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/ConfigMapper.java +303 -0
- package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/HeadlessTaskRegistry.java +34 -0
- package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/JsEvaluatorTaskRunner.java +63 -0
- package/android/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +699 -0
- package/android/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +103 -0
- package/android/src/main/java/com/marianhello/bgloc/Config.java +1155 -0
- package/android/src/main/java/com/marianhello/bgloc/ConnectivityListener.java +5 -0
- package/android/src/main/java/com/marianhello/bgloc/HttpPostService.java +362 -0
- package/android/src/main/java/com/marianhello/bgloc/LocationManager.java +138 -0
- package/android/src/main/java/com/marianhello/bgloc/PluginDelegate.java +45 -0
- package/android/src/main/java/com/marianhello/bgloc/PluginException.java +38 -0
- package/android/src/main/java/com/marianhello/bgloc/PostLocationTask.java +238 -0
- package/android/src/main/java/com/marianhello/bgloc/ResourceResolver.java +55 -0
- package/android/src/main/java/com/marianhello/bgloc/data/AbstractLocationTemplate.java +69 -0
- package/android/src/main/java/com/marianhello/bgloc/data/ArrayListLocationTemplate.java +88 -0
- package/android/src/main/java/com/marianhello/bgloc/data/BackgroundActivity.java +108 -0
- package/android/src/main/java/com/marianhello/bgloc/data/BackgroundLocation.java +1088 -0
- package/android/src/main/java/com/marianhello/bgloc/data/ConfigJsonMapper.java +211 -0
- package/android/src/main/java/com/marianhello/bgloc/data/ConfigurationDAO.java +13 -0
- package/android/src/main/java/com/marianhello/bgloc/data/DAOFactory.java +17 -0
- package/android/src/main/java/com/marianhello/bgloc/data/HashMapLocationTemplate.java +82 -0
- package/android/src/main/java/com/marianhello/bgloc/data/LocationDAO.java +27 -0
- package/android/src/main/java/com/marianhello/bgloc/data/LocationTemplate.java +12 -0
- package/android/src/main/java/com/marianhello/bgloc/data/LocationTemplateFactory.java +71 -0
- package/android/src/main/java/com/marianhello/bgloc/data/LocationTransform.java +19 -0
- package/android/src/main/java/com/marianhello/bgloc/data/SessionLocationDAO.java +18 -0
- package/android/src/main/java/com/marianhello/bgloc/data/provider/ContentProviderLocationDAO.java +406 -0
- package/android/src/main/java/com/marianhello/bgloc/data/provider/LocationContentProvider.java +321 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationContract.java +94 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationDAO.java +227 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationContract.java +122 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationDAO.java +550 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +189 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionContract.java +74 -0
- package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionLocationDAO.java +169 -0
- package/android/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/AbstractTaskRunner.java +15 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/ActivityTask.java +48 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/JsCallback.java +10 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/LocationTask.java +60 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/StationaryTask.java +25 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/Task.java +8 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/TaskRunner.java +5 -0
- package/android/src/main/java/com/marianhello/bgloc/headless/TaskRunnerFactory.java +8 -0
- package/android/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
- package/android/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/AbstractLocationProvider.java +218 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +385 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +685 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/LocationProvider.java +32 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/LocationProviderFactory.java +47 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/ProviderDelegate.java +12 -0
- package/android/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +175 -0
- package/android/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
- package/android/src/main/java/com/marianhello/bgloc/service/LocationService.java +16 -0
- package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +1531 -0
- package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceInfo.java +6 -0
- package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceInfoImpl.java +41 -0
- package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceIntentBuilder.java +203 -0
- package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +156 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/AccountHelper.java +39 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/Authenticator.java +68 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/AuthenticatorService.java +28 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/BatchManager.java +311 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/NotificationHelper.java +148 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +301 -0
- package/android/src/main/java/com/marianhello/bgloc/sync/SyncService.java +68 -0
- package/android/src/main/java/com/marianhello/logging/DBLogReader.java +208 -0
- package/android/src/main/java/com/marianhello/logging/LogEntry.java +99 -0
- package/android/src/main/java/com/marianhello/logging/LoggerManager.java +70 -0
- package/android/src/main/java/com/marianhello/logging/UncaughtExceptionLogger.java +36 -0
- package/android/src/main/java/com/marianhello/utils/CloneHelper.java +22 -0
- package/android/src/main/java/com/marianhello/utils/Convert.java +56 -0
- package/android/src/main/java/com/marianhello/utils/TextUtils.java +72 -0
- package/android/src/main/java/com/marianhello/utils/ToneGenerator.java +68 -0
- package/android/src/main/java/org/apache/commons/io/Charsets.java +153 -0
- package/android/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java +344 -0
- package/android/src/main/java/org/chromium/content/browser/ThreadUtils.java +134 -0
- package/android/src/main/java/ru/andremoniy/sqlbuilder/SqlExpression.java +398 -0
- package/android/src/main/java/ru/andremoniy/sqlbuilder/SqlSelectStatement.java +671 -0
- package/android/src/main/java/ru/andremoniy/sqlbuilder/SqlStatement.java +29 -0
- package/android/src/main/java/ru/andremoniy/utils/TextUtils.java +61 -0
- package/android/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/android/src/main/res/values/strings.xml +15 -0
- package/android/src/main/res/xml/authenticator.xml +7 -0
- package/android/src/main/res/xml/syncadapter.xml +9 -0
- package/dist/esm/definitions.d.ts +1052 -0
- package/dist/esm/definitions.js +142 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +23 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +92 -0
- package/dist/esm/web.js +242 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +415 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +418 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/BackgroundGeolocationPlugin/BackgroundGeolocationPlugin-Bridging-Header.h +18 -0
- package/ios/Sources/BackgroundGeolocationPlugin/BackgroundGeolocationPlugin.m +52 -0
- package/ios/Sources/BackgroundGeolocationPlugin/BackgroundGeolocationPlugin.swift +750 -0
- package/ios/Tests/BackgroundGeolocationPluginTests/BackgroundGeolocationPluginTests.swift +12 -0
- package/ios/common/BackgroundGeolocation/CocoaLumberjack.h +1945 -0
- package/ios/common/BackgroundGeolocation/CocoaLumberjack.m +5255 -0
- package/ios/common/BackgroundGeolocation/FMDB.h +2357 -0
- package/ios/common/BackgroundGeolocation/FMDB.m +2672 -0
- package/ios/common/BackgroundGeolocation/FMDBLogger.h +42 -0
- package/ios/common/BackgroundGeolocation/FMDBLogger.m +264 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTUHeadingRequest.h +41 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTUHeadingRequest.m +68 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationManager+Internal.h +33 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationManager.h +178 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationManager.m +1025 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationRequest.h +103 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationRequest.m +238 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationRequestDefines.h +163 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTURequestIDGenerator.h +39 -0
- package/ios/common/BackgroundGeolocation/INTULocationManager/INTURequestIDGenerator.m +37 -0
- package/ios/common/BackgroundGeolocation/MAURAbstractLocationProvider.h +51 -0
- package/ios/common/BackgroundGeolocation/MAURAbstractLocationProvider.m +53 -0
- package/ios/common/BackgroundGeolocation/MAURActivity.h +23 -0
- package/ios/common/BackgroundGeolocation/MAURActivity.m +52 -0
- package/ios/common/BackgroundGeolocation/MAURActivityLocationProvider.h +18 -0
- package/ios/common/BackgroundGeolocation/MAURActivityLocationProvider.m +340 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +88 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +1193 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +46 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +283 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundTaskManager.h +25 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundTaskManager.m +105 -0
- package/ios/common/BackgroundGeolocation/MAURConfig.h +99 -0
- package/ios/common/BackgroundGeolocation/MAURConfig.m +636 -0
- package/ios/common/BackgroundGeolocation/MAURConfigurationContract.h +53 -0
- package/ios/common/BackgroundGeolocation/MAURConfigurationContract.m +54 -0
- package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.h +20 -0
- package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +550 -0
- package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.h +17 -0
- package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +124 -0
- package/ios/common/BackgroundGeolocation/MAURLocation.h +73 -0
- package/ios/common/BackgroundGeolocation/MAURLocation.m +392 -0
- package/ios/common/BackgroundGeolocation/MAURLocationContract.h +38 -0
- package/ios/common/BackgroundGeolocation/MAURLocationContract.m +39 -0
- package/ios/common/BackgroundGeolocation/MAURLocationManager.h +53 -0
- package/ios/common/BackgroundGeolocation/MAURLocationManager.m +305 -0
- package/ios/common/BackgroundGeolocation/MAURLogReader.h +26 -0
- package/ios/common/BackgroundGeolocation/MAURLogReader.m +122 -0
- package/ios/common/BackgroundGeolocation/MAURLogging.h +19 -0
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +53 -0
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +367 -0
- package/ios/common/BackgroundGeolocation/MAURProviderDelegate.h +52 -0
- package/ios/common/BackgroundGeolocation/MAURRawLocationProvider.h +18 -0
- package/ios/common/BackgroundGeolocation/MAURRawLocationProvider.m +138 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.h +26 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.m +335 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteHelper.h +57 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteHelper.m +93 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.h +52 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.m +520 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteOpenHelper.h +32 -0
- package/ios/common/BackgroundGeolocation/MAURSQLiteOpenHelper.m +276 -0
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.h +29 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.m +31 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.h +25 -0
- package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.m +153 -0
- package/ios/common/BackgroundGeolocation/MAURUncaughtExceptionLogger.h +20 -0
- package/ios/common/BackgroundGeolocation/MAURUncaughtExceptionLogger.m +62 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
- package/ios/common/BackgroundGeolocation/Reachability.h +102 -0
- package/ios/common/BackgroundGeolocation/Reachability.m +475 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/README.md +170 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/ext/NSString+ZIMString.h +55 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/ext/NSString+ZIMString.m +47 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlDataManipulationCommand.h +27 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlExpression.h +250 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlExpression.m +259 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlSelectStatement.h +360 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlSelectStatement.m +427 -0
- package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlStatement.h +37 -0
- package/ios/common/BackgroundGeolocation/module.modulemap +16 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
/*
|
|
2
|
+
According to apache license
|
|
3
|
+
|
|
4
|
+
This is fork of christocracy cordova-plugin-background-geolocation plugin
|
|
5
|
+
https://github.com/christocracy/cordova-plugin-background-geolocation
|
|
6
|
+
|
|
7
|
+
This is a new class
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
package com.marianhello.bgloc.service;
|
|
11
|
+
|
|
12
|
+
import android.accounts.Account;
|
|
13
|
+
import android.app.Notification;
|
|
14
|
+
import android.app.NotificationManager;
|
|
15
|
+
import android.app.Service;
|
|
16
|
+
import android.content.BroadcastReceiver;
|
|
17
|
+
import android.content.ContentResolver;
|
|
18
|
+
import android.content.Context;
|
|
19
|
+
import android.content.Intent;
|
|
20
|
+
import android.content.IntentFilter;
|
|
21
|
+
import android.content.ComponentName;
|
|
22
|
+
import android.content.pm.PackageManager;
|
|
23
|
+
import android.content.pm.ServiceInfo;
|
|
24
|
+
import android.Manifest;
|
|
25
|
+
import android.location.Location;
|
|
26
|
+
import android.net.ConnectivityManager;
|
|
27
|
+
import android.net.NetworkInfo;
|
|
28
|
+
import android.os.Binder;
|
|
29
|
+
import android.os.Build;
|
|
30
|
+
import android.os.Bundle;
|
|
31
|
+
import android.os.Handler;
|
|
32
|
+
import android.os.HandlerThread;
|
|
33
|
+
import android.os.IBinder;
|
|
34
|
+
import android.os.Looper;
|
|
35
|
+
import android.os.Message;
|
|
36
|
+
import android.os.PowerManager;
|
|
37
|
+
import android.os.Process;
|
|
38
|
+
import androidx.annotation.Nullable;
|
|
39
|
+
import androidx.core.content.ContextCompat;
|
|
40
|
+
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
41
|
+
|
|
42
|
+
import com.marianhello.bgloc.Config;
|
|
43
|
+
import com.marianhello.bgloc.ConnectivityListener;
|
|
44
|
+
import com.marianhello.bgloc.sync.NotificationHelper;
|
|
45
|
+
import com.marianhello.bgloc.PluginException;
|
|
46
|
+
import com.marianhello.bgloc.PostLocationTask;
|
|
47
|
+
import com.marianhello.bgloc.ResourceResolver;
|
|
48
|
+
import com.marianhello.bgloc.data.BackgroundActivity;
|
|
49
|
+
import com.marianhello.bgloc.data.BackgroundLocation;
|
|
50
|
+
import com.marianhello.bgloc.data.ConfigurationDAO;
|
|
51
|
+
import com.marianhello.bgloc.data.DAOFactory;
|
|
52
|
+
import com.marianhello.bgloc.data.LocationDAO;
|
|
53
|
+
import com.marianhello.bgloc.data.SessionLocationDAO;
|
|
54
|
+
import com.marianhello.bgloc.data.LocationTransform;
|
|
55
|
+
import com.marianhello.bgloc.data.sqlite.SQLiteSessionLocationDAO;
|
|
56
|
+
import com.marianhello.bgloc.headless.AbstractTaskRunner;
|
|
57
|
+
import com.marianhello.bgloc.headless.ActivityTask;
|
|
58
|
+
import com.marianhello.bgloc.headless.LocationTask;
|
|
59
|
+
import com.marianhello.bgloc.headless.StationaryTask;
|
|
60
|
+
import com.marianhello.bgloc.headless.Task;
|
|
61
|
+
import com.marianhello.bgloc.headless.TaskRunner;
|
|
62
|
+
import com.marianhello.bgloc.headless.TaskRunnerFactory;
|
|
63
|
+
import com.marianhello.bgloc.provider.LocationProvider;
|
|
64
|
+
import com.marianhello.bgloc.provider.LocationProviderFactory;
|
|
65
|
+
import com.marianhello.bgloc.provider.ProviderDelegate;
|
|
66
|
+
import com.marianhello.bgloc.sync.AccountHelper;
|
|
67
|
+
import com.marianhello.bgloc.sync.SyncService;
|
|
68
|
+
import com.marianhello.logging.LoggerManager;
|
|
69
|
+
import com.marianhello.logging.UncaughtExceptionLogger;
|
|
70
|
+
|
|
71
|
+
import org.chromium.content.browser.ThreadUtils;
|
|
72
|
+
import org.json.JSONException;
|
|
73
|
+
|
|
74
|
+
import java.util.Locale;
|
|
75
|
+
|
|
76
|
+
import static com.marianhello.bgloc.service.LocationServiceIntentBuilder.containsCommand;
|
|
77
|
+
import static com.marianhello.bgloc.service.LocationServiceIntentBuilder.containsMessage;
|
|
78
|
+
import static com.marianhello.bgloc.service.LocationServiceIntentBuilder.getCommand;
|
|
79
|
+
import static com.marianhello.bgloc.service.LocationServiceIntentBuilder.getMessage;
|
|
80
|
+
|
|
81
|
+
public class LocationServiceImpl extends Service implements ProviderDelegate, LocationService {
|
|
82
|
+
|
|
83
|
+
public static final String ACTION_BROADCAST = ".broadcast";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* CommandId sent by the service to
|
|
87
|
+
* any registered clients with error.
|
|
88
|
+
*/
|
|
89
|
+
public static final int MSG_ON_ERROR = 100;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* CommandId sent by the service to
|
|
93
|
+
* any registered clients with the new position.
|
|
94
|
+
*/
|
|
95
|
+
public static final int MSG_ON_LOCATION = 101;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* CommandId sent by the service to
|
|
99
|
+
* any registered clients whenever the devices enters "stationary-mode"
|
|
100
|
+
*/
|
|
101
|
+
public static final int MSG_ON_STATIONARY = 102;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* CommandId sent by the service to
|
|
105
|
+
* any registered clients with new detected activity.
|
|
106
|
+
*/
|
|
107
|
+
public static final int MSG_ON_ACTIVITY = 103;
|
|
108
|
+
|
|
109
|
+
public static final int MSG_ON_SERVICE_STARTED = 104;
|
|
110
|
+
|
|
111
|
+
public static final int MSG_ON_SERVICE_STOPPED = 105;
|
|
112
|
+
|
|
113
|
+
public static final int MSG_ON_ABORT_REQUESTED = 106;
|
|
114
|
+
|
|
115
|
+
public static final int MSG_ON_HTTP_AUTHORIZATION = 107;
|
|
116
|
+
|
|
117
|
+
/** v3.5 Phase 4: sync queue events. */
|
|
118
|
+
public static final int MSG_ON_SYNC_START = 108;
|
|
119
|
+
public static final int MSG_ON_SYNC_SUCCESS = 109;
|
|
120
|
+
public static final int MSG_ON_SYNC_ERROR = 110;
|
|
121
|
+
public static final int MSG_ON_SYNC_PROGRESS = 111;
|
|
122
|
+
public static final int MSG_ON_HEARTBEAT = 112;
|
|
123
|
+
/** v4.0 Phase 6 — driver insight events. */
|
|
124
|
+
public static final int MSG_ON_TRIP_START = 113;
|
|
125
|
+
public static final int MSG_ON_TRIP_END = 114;
|
|
126
|
+
public static final int MSG_ON_MOVING = 115;
|
|
127
|
+
public static final int MSG_ON_STOPPED = 116;
|
|
128
|
+
public static final int MSG_ON_SPEEDING = 117;
|
|
129
|
+
public static final int MSG_ON_PROVIDER_CHANGE = 118;
|
|
130
|
+
public static final int MSG_ON_SOS = 119;
|
|
131
|
+
/** v4.1 — sensor-like GPS-derived driving events. */
|
|
132
|
+
public static final int MSG_ON_HARD_BRAKE = 120;
|
|
133
|
+
public static final int MSG_ON_RAPID_ACCELERATION = 121;
|
|
134
|
+
public static final int MSG_ON_SHARP_TURN = 122;
|
|
135
|
+
public static final int MSG_ON_POSSIBLE_CRASH = 123;
|
|
136
|
+
/** v4.2 — sensor-fusion-only events. {@code MSG_ON_POSSIBLE_CRASH} is reused
|
|
137
|
+
* by the sensor pipeline; phone-usage is a brand-new event. */
|
|
138
|
+
public static final int MSG_ON_PHONE_USAGE_WHILE_DRIVING = 124;
|
|
139
|
+
|
|
140
|
+
/** notification id */
|
|
141
|
+
private static int NOTIFICATION_ID = 1;
|
|
142
|
+
|
|
143
|
+
private ResourceResolver mResolver;
|
|
144
|
+
private Config mConfig;
|
|
145
|
+
private LocationProvider mProvider;
|
|
146
|
+
private Account mSyncAccount;
|
|
147
|
+
|
|
148
|
+
private org.slf4j.Logger logger;
|
|
149
|
+
|
|
150
|
+
private final IBinder mBinder = new LocalBinder();
|
|
151
|
+
private HandlerThread mHandlerThread;
|
|
152
|
+
private ServiceHandler mServiceHandler;
|
|
153
|
+
private LocationDAO mLocationDAO;
|
|
154
|
+
private SessionLocationDAO mSessionDAO;
|
|
155
|
+
private PostLocationTask mPostLocationTask;
|
|
156
|
+
private String mHeadlessTaskRunnerClass;
|
|
157
|
+
private TaskRunner mHeadlessTaskRunner;
|
|
158
|
+
|
|
159
|
+
private long mServiceId = -1;
|
|
160
|
+
private static boolean sIsRunning = false;
|
|
161
|
+
private boolean mIsInForeground = false;
|
|
162
|
+
|
|
163
|
+
private PowerManager.WakeLock mWakeLock;
|
|
164
|
+
private static final String WAKE_LOCK_TAG = "com.marianhello.bgloc:LocationServiceWakeLock";
|
|
165
|
+
|
|
166
|
+
/** Last time we received a location (for watchdog). */
|
|
167
|
+
private volatile long mLastLocationTime = 0L;
|
|
168
|
+
/** v3.5 Phase 4: latest received location, used as heartbeat payload. */
|
|
169
|
+
private volatile BackgroundLocation mLastReceivedLocation;
|
|
170
|
+
/** v4.0 Phase 6: static accessor for {@link com.marianhello.bgloc.BackgroundGeolocationFacade#triggerSOS}. */
|
|
171
|
+
private static volatile BackgroundLocation sLastReceivedLocation;
|
|
172
|
+
public static BackgroundLocation getLastReceivedLocation() { return sLastReceivedLocation; }
|
|
173
|
+
/** v3.5 Phase 4: heartbeat scheduler. */
|
|
174
|
+
private java.util.concurrent.ScheduledExecutorService mHeartbeatExecutor;
|
|
175
|
+
private java.util.concurrent.ScheduledFuture<?> mHeartbeatTask;
|
|
176
|
+
|
|
177
|
+
/** v4.0 Phase 6: driver-insights detector. Created lazily when config has drivingEvents.enabled. */
|
|
178
|
+
private com.marianhello.bgloc.driving.DrivingEventsDetector mDrivingDetector;
|
|
179
|
+
/** v4.2 Phase 8: real sensor-fusion detector. Created when drivingEvents.sensorFusion=true. */
|
|
180
|
+
private com.marianhello.bgloc.sensor.SensorFusionDetector mSensorFusion;
|
|
181
|
+
/** v4.2 Phase 8: cached tripActive state so hot-reload can re-inject it. */
|
|
182
|
+
private volatile boolean mDrivingTripActive = false;
|
|
183
|
+
/** v4.3: events fired without a simultaneous fix (providerChange, sensor crash, phone usage,
|
|
184
|
+
* manual SOS) buffered here and flushed onto the next location's `events` array.
|
|
185
|
+
* v4.4.1: capped at PENDING_DRIVING_EVENTS_MAX entries (oldest evicted) and entries older
|
|
186
|
+
* than PENDING_DRIVING_EVENTS_TTL_MS are dropped at flush time. */
|
|
187
|
+
private final org.json.JSONArray mPendingDrivingEvents = new org.json.JSONArray();
|
|
188
|
+
private static final int PENDING_DRIVING_EVENTS_MAX = 20;
|
|
189
|
+
private static final long PENDING_DRIVING_EVENTS_TTL_MS = 60_000L;
|
|
190
|
+
private static final long WATCHDOG_INTERVAL_MS = 60_000L;
|
|
191
|
+
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
|
|
192
|
+
private final Runnable mWatchdogRunnable = new Runnable() {
|
|
193
|
+
@Override
|
|
194
|
+
public void run() {
|
|
195
|
+
if (!sIsRunning || mProvider == null || mConfig == null) return;
|
|
196
|
+
if (!Boolean.TRUE.equals(mConfig.getEnableWatchdog())) return;
|
|
197
|
+
long now = System.currentTimeMillis();
|
|
198
|
+
if (mLastLocationTime > 0 && (now - mLastLocationTime) > WATCHDOG_INTERVAL_MS) {
|
|
199
|
+
// v4.5.1: when drivingEvents is enabled, treat "no fixes" while NOT tripActive as
|
|
200
|
+
// intentional stationary → don't restart (saves battery). When drivingEvents is
|
|
201
|
+
// disabled (the plugin has no notion of "trip"), keep the legacy behaviour of
|
|
202
|
+
// restarting on every stale window.
|
|
203
|
+
Config.DrivingEventsOptions de = mConfig.getDrivingEvents();
|
|
204
|
+
boolean drivingEnabled = de != null && de.enabled;
|
|
205
|
+
boolean shouldRestart = !drivingEnabled || mDrivingTripActive;
|
|
206
|
+
if (shouldRestart) {
|
|
207
|
+
logger.info("Location watchdog: no update in {}s, restarting provider", WATCHDOG_INTERVAL_MS / 1000);
|
|
208
|
+
try {
|
|
209
|
+
mProvider.onStop();
|
|
210
|
+
mProvider.onStart();
|
|
211
|
+
} catch (Exception e) {
|
|
212
|
+
logger.warn("Watchdog restart failed", e);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
logger.debug("Location watchdog: stationary (no active trip); skipping restart");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
mMainHandler.postDelayed(this, WATCHDOG_INTERVAL_MS);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Session start time for notification elapsed time (showTime). */
|
|
223
|
+
private volatile long mSessionStartTime = 0L;
|
|
224
|
+
/** Accumulated distance in meters for notification (showDistance). */
|
|
225
|
+
private volatile double mSessionDistanceMeters = 0.0;
|
|
226
|
+
private volatile double mLastLat = 0.0;
|
|
227
|
+
private volatile double mLastLon = 0.0;
|
|
228
|
+
private volatile boolean mHasLastLocation = false;
|
|
229
|
+
private static final long NOTIFICATION_UPDATE_INTERVAL_MS = 1000L;
|
|
230
|
+
private final Runnable mNotificationUpdateRunnable = new Runnable() {
|
|
231
|
+
@Override
|
|
232
|
+
public void run() {
|
|
233
|
+
if (!sIsRunning || !mIsInForeground || mConfig == null) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
boolean showTime = Boolean.TRUE.equals(mConfig.getShowTime());
|
|
237
|
+
boolean showDistance = Boolean.TRUE.equals(mConfig.getShowDistance());
|
|
238
|
+
if (!showTime && !showDistance) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
updateForegroundNotification();
|
|
242
|
+
mMainHandler.postDelayed(this, NOTIFICATION_UPDATE_INTERVAL_MS);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
private static LocationTransform sLocationTransform;
|
|
247
|
+
private static LocationProviderFactory sLocationProviderFactory;
|
|
248
|
+
|
|
249
|
+
private class ServiceHandler extends Handler {
|
|
250
|
+
public ServiceHandler(Looper looper) {
|
|
251
|
+
super(looper);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@Override
|
|
255
|
+
public void handleMessage(Message msg) {
|
|
256
|
+
super.handleMessage(msg);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* When binding to the service, we return an interface to our messenger
|
|
262
|
+
* for sending messages to the service.
|
|
263
|
+
*/
|
|
264
|
+
@Override
|
|
265
|
+
public IBinder onBind(Intent intent) {
|
|
266
|
+
logger.debug("Client binds to service");
|
|
267
|
+
return mBinder;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Override
|
|
271
|
+
public void onRebind(Intent intent) {
|
|
272
|
+
logger.debug("Client rebinds to service");
|
|
273
|
+
super.onRebind(intent);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@Override
|
|
277
|
+
public boolean onUnbind(Intent intent) {
|
|
278
|
+
// All clients have unbound with unbindService()
|
|
279
|
+
logger.debug("All clients have been unbound from service");
|
|
280
|
+
|
|
281
|
+
return true; // Ensures onRebind() is called when a client re-binds.
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@Override
|
|
285
|
+
public void onCreate() {
|
|
286
|
+
super.onCreate();
|
|
287
|
+
|
|
288
|
+
sIsRunning = false;
|
|
289
|
+
|
|
290
|
+
UncaughtExceptionLogger.register(this);
|
|
291
|
+
|
|
292
|
+
logger = LoggerManager.getLogger(LocationServiceImpl.class);
|
|
293
|
+
logger.info("Creating LocationServiceImpl");
|
|
294
|
+
|
|
295
|
+
mServiceId = System.currentTimeMillis();
|
|
296
|
+
|
|
297
|
+
// Start up the thread running the service. Note that we create a
|
|
298
|
+
// separate thread because the service normally runs in the process's
|
|
299
|
+
// main thread, which we don't want to block. We also make it
|
|
300
|
+
// background priority so CPU-intensive work will not disrupt our UI.
|
|
301
|
+
if (mHandlerThread == null) {
|
|
302
|
+
mHandlerThread = new HandlerThread("LocationServiceImpl.Thread", Process.THREAD_PRIORITY_BACKGROUND);
|
|
303
|
+
}
|
|
304
|
+
mHandlerThread.start();
|
|
305
|
+
// An Android service handler is a handler running on a specific background thread.
|
|
306
|
+
mServiceHandler = new ServiceHandler(mHandlerThread.getLooper());
|
|
307
|
+
|
|
308
|
+
mResolver = ResourceResolver.newInstance(this);
|
|
309
|
+
|
|
310
|
+
mSyncAccount = AccountHelper.CreateSyncAccount(this, mResolver.getAccountName(),
|
|
311
|
+
mResolver.getAccountType());
|
|
312
|
+
|
|
313
|
+
String authority = mResolver.getAuthority();
|
|
314
|
+
ContentResolver.setIsSyncable(mSyncAccount, authority, 1);
|
|
315
|
+
ContentResolver.setSyncAutomatically(mSyncAccount, authority, true);
|
|
316
|
+
|
|
317
|
+
mLocationDAO = DAOFactory.createLocationDAO(this);
|
|
318
|
+
mSessionDAO = new SQLiteSessionLocationDAO(this);
|
|
319
|
+
|
|
320
|
+
mPostLocationTask = new PostLocationTask(mLocationDAO, mSessionDAO,
|
|
321
|
+
new PostLocationTask.PostLocationTaskListener() {
|
|
322
|
+
@Override
|
|
323
|
+
public void onRequestedAbortUpdates() {
|
|
324
|
+
handleRequestedAbortUpdates();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@Override
|
|
328
|
+
public void onHttpAuthorizationUpdates() {
|
|
329
|
+
handleHttpAuthorizationUpdates();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@Override
|
|
333
|
+
public void onSyncRequested() {
|
|
334
|
+
SyncService.sync(mSyncAccount, mResolver.getAuthority(), false);
|
|
335
|
+
}
|
|
336
|
+
}, new ConnectivityListener() {
|
|
337
|
+
@Override
|
|
338
|
+
public boolean hasConnectivity() {
|
|
339
|
+
return isNetworkAvailable();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
registerReceiver(connectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
|
344
|
+
NotificationHelper.registerServiceChannel(this);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@Override
|
|
348
|
+
public void onDestroy() {
|
|
349
|
+
logger.info("Destroying LocationServiceImpl");
|
|
350
|
+
|
|
351
|
+
// workaround for issue #276
|
|
352
|
+
if (mProvider != null) {
|
|
353
|
+
mProvider.onDestroy();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (mHandlerThread != null) {
|
|
357
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
358
|
+
mHandlerThread.quitSafely();
|
|
359
|
+
} else {
|
|
360
|
+
mHandlerThread.quit(); //sorry
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (mPostLocationTask != null) {
|
|
365
|
+
mPostLocationTask.shutdown();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
unregisterReceiver(connectivityChangeReceiver);
|
|
370
|
+
|
|
371
|
+
sIsRunning = false;
|
|
372
|
+
super.onDestroy();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@Override
|
|
376
|
+
public void onTaskRemoved(Intent rootIntent) {
|
|
377
|
+
logger.debug("Task has been removed");
|
|
378
|
+
// workaround for issue #276
|
|
379
|
+
Config config = getConfig();
|
|
380
|
+
if (config.getStopOnTerminate()) {
|
|
381
|
+
logger.info("Stopping self");
|
|
382
|
+
stopSelf();
|
|
383
|
+
} else {
|
|
384
|
+
logger.info("Continue running in background");
|
|
385
|
+
}
|
|
386
|
+
super.onTaskRemoved(rootIntent);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@Override
|
|
390
|
+
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
391
|
+
if (intent == null) {
|
|
392
|
+
// when service was killed and restarted we will restart service
|
|
393
|
+
start();
|
|
394
|
+
return START_STICKY;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
boolean containsCommand = containsCommand(intent);
|
|
398
|
+
logger.debug(
|
|
399
|
+
String.format("Service in [%s] state. cmdId: [%s]. startId: [%d]",
|
|
400
|
+
sIsRunning ? "STARTED" : "NOT STARTED",
|
|
401
|
+
containsCommand ? getCommand(intent).getId() : "N/A",
|
|
402
|
+
startId)
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (containsCommand) {
|
|
406
|
+
LocationServiceIntentBuilder.Command cmd = getCommand(intent);
|
|
407
|
+
processCommand(cmd.getId(), cmd.getArgument());
|
|
408
|
+
} else {
|
|
409
|
+
// Could be a BOOT-event, or the OS just randomly restarted the service...
|
|
410
|
+
startForegroundService();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (containsMessage(intent)) {
|
|
414
|
+
processMessage(getMessage(intent));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return START_STICKY;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private void processMessage(String message) {
|
|
421
|
+
// currently we do not process any message
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private void processCommand(int command, Object arg) {
|
|
425
|
+
try {
|
|
426
|
+
switch (command) {
|
|
427
|
+
case CommandId.START:
|
|
428
|
+
start();
|
|
429
|
+
break;
|
|
430
|
+
case CommandId.START_FOREGROUND_SERVICE:
|
|
431
|
+
startForegroundService();
|
|
432
|
+
break;
|
|
433
|
+
case CommandId.STOP:
|
|
434
|
+
stop();
|
|
435
|
+
break;
|
|
436
|
+
case CommandId.CONFIGURE:
|
|
437
|
+
configure((Config) arg);
|
|
438
|
+
break;
|
|
439
|
+
case CommandId.STOP_FOREGROUND:
|
|
440
|
+
stopForeground();
|
|
441
|
+
break;
|
|
442
|
+
case CommandId.START_FOREGROUND:
|
|
443
|
+
startForeground();
|
|
444
|
+
break;
|
|
445
|
+
case CommandId.REGISTER_HEADLESS_TASK:
|
|
446
|
+
registerHeadlessTask((String) arg);
|
|
447
|
+
break;
|
|
448
|
+
case CommandId.START_HEADLESS_TASK:
|
|
449
|
+
startHeadlessTask();
|
|
450
|
+
break;
|
|
451
|
+
case CommandId.STOP_HEADLESS_TASK:
|
|
452
|
+
stopHeadlessTask();
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
} catch (Exception e) {
|
|
456
|
+
logger.error("processCommand: exception", e);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@Override
|
|
461
|
+
public synchronized void start() {
|
|
462
|
+
if (sIsRunning) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (mConfig == null) {
|
|
467
|
+
logger.warn("Attempt to start unconfigured service. Will use stored or default.");
|
|
468
|
+
mConfig = getConfig();
|
|
469
|
+
// TODO: throw JSONException if config cannot be obtained from db
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
logger.debug("Will start service with: {}", mConfig.toString());
|
|
473
|
+
|
|
474
|
+
if (!hasLocationPermission()) {
|
|
475
|
+
logger.warn("Cannot start location service: ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION not granted");
|
|
476
|
+
stopSelf();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
mPostLocationTask.setConfig(mConfig);
|
|
481
|
+
mPostLocationTask.clearQueue();
|
|
482
|
+
|
|
483
|
+
LocationProviderFactory spf = sLocationProviderFactory != null
|
|
484
|
+
? sLocationProviderFactory : new LocationProviderFactory(this);
|
|
485
|
+
mProvider = spf.getInstance(mConfig.getLocationProvider());
|
|
486
|
+
mProvider.setDelegate(this);
|
|
487
|
+
mProvider.onCreate();
|
|
488
|
+
mProvider.onConfigure(mConfig);
|
|
489
|
+
|
|
490
|
+
sIsRunning = true;
|
|
491
|
+
|
|
492
|
+
if (mWakeLock == null) {
|
|
493
|
+
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
|
494
|
+
if (pm != null) {
|
|
495
|
+
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// v4.5.1: only hold a permanent CPU wake lock when wakeLockMode == 'always'.
|
|
499
|
+
// Default 'posting' acquires only briefly during onLocation/post; 'none' never.
|
|
500
|
+
String wlMode = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
|
|
501
|
+
if ("always".equals(wlMode) && mWakeLock != null && !mWakeLock.isHeld()) {
|
|
502
|
+
mWakeLock.acquire();
|
|
503
|
+
logger.debug("Wake lock acquired (mode=always)");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (Boolean.TRUE.equals(mConfig.getEnableWatchdog())) {
|
|
507
|
+
mLastLocationTime = System.currentTimeMillis();
|
|
508
|
+
mMainHandler.removeCallbacks(mWatchdogRunnable);
|
|
509
|
+
mMainHandler.postDelayed(mWatchdogRunnable, WATCHDOG_INTERVAL_MS);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
mSessionStartTime = System.currentTimeMillis();
|
|
513
|
+
mSessionDistanceMeters = 0.0;
|
|
514
|
+
mLastLat = 0.0;
|
|
515
|
+
mLastLon = 0.0;
|
|
516
|
+
mHasLastLocation = false;
|
|
517
|
+
|
|
518
|
+
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
|
|
519
|
+
@Override
|
|
520
|
+
public void run() {
|
|
521
|
+
mProvider.onStart();
|
|
522
|
+
if (mConfig.getStartForeground()) {
|
|
523
|
+
startForeground();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
Bundle bundle = new Bundle();
|
|
529
|
+
bundle.putInt("action", MSG_ON_SERVICE_STARTED);
|
|
530
|
+
bundle.putLong("serviceId", mServiceId);
|
|
531
|
+
broadcastMessage(bundle);
|
|
532
|
+
|
|
533
|
+
// v3.5 Phase 4: kick off heartbeat scheduler when the service starts.
|
|
534
|
+
scheduleHeartbeat();
|
|
535
|
+
|
|
536
|
+
// v4.0 Phase 6: build driver-insights detector if enabled in config.
|
|
537
|
+
configureDrivingDetector();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** v4.0 Phase 6: instantiate / reconfigure the GPS-based driver-insights detector. */
|
|
541
|
+
private void configureDrivingDetector() {
|
|
542
|
+
if (mConfig == null) return;
|
|
543
|
+
com.marianhello.bgloc.Config.DrivingEventsOptions opts = mConfig.getDrivingEvents();
|
|
544
|
+
if (opts == null || !opts.enabled) {
|
|
545
|
+
if (mDrivingDetector != null) mDrivingDetector.reset();
|
|
546
|
+
mDrivingDetector = null;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
com.marianhello.bgloc.driving.DrivingEventsDetector.Config c =
|
|
550
|
+
new com.marianhello.bgloc.driving.DrivingEventsDetector.Config();
|
|
551
|
+
c.enabled = true;
|
|
552
|
+
c.speedLimitKmh = opts.speedLimitKmh;
|
|
553
|
+
c.minMovingSpeedMps = opts.minMovingSpeedMps;
|
|
554
|
+
c.stoppedDurationMs = opts.stoppedDurationMs;
|
|
555
|
+
c.minTripSpeedMps = opts.minTripSpeedMps;
|
|
556
|
+
c.minTripDurationMs = opts.minTripDurationMs;
|
|
557
|
+
|
|
558
|
+
mDrivingDetector = new com.marianhello.bgloc.driving.DrivingEventsDetector(
|
|
559
|
+
new com.marianhello.bgloc.driving.DrivingEventsDetector.Listener() {
|
|
560
|
+
@Override public void onMoving(BackgroundLocation l) {
|
|
561
|
+
attachDrivingEvent(l, "moving", null);
|
|
562
|
+
Bundle b = new Bundle();
|
|
563
|
+
b.putInt("action", MSG_ON_MOVING);
|
|
564
|
+
if (l != null) b.putParcelable("payload", l);
|
|
565
|
+
broadcastMessage(b);
|
|
566
|
+
}
|
|
567
|
+
@Override public void onStopped(BackgroundLocation l) {
|
|
568
|
+
attachDrivingEvent(l, "stopped", null);
|
|
569
|
+
Bundle b = new Bundle();
|
|
570
|
+
b.putInt("action", MSG_ON_STOPPED);
|
|
571
|
+
if (l != null) b.putParcelable("payload", l);
|
|
572
|
+
broadcastMessage(b);
|
|
573
|
+
}
|
|
574
|
+
@Override public void onTripStart(BackgroundLocation l) {
|
|
575
|
+
attachDrivingEvent(l, "tripStart", null);
|
|
576
|
+
Bundle b = new Bundle();
|
|
577
|
+
b.putInt("action", MSG_ON_TRIP_START);
|
|
578
|
+
if (l != null) b.putParcelable("payload", l);
|
|
579
|
+
broadcastMessage(b);
|
|
580
|
+
mDrivingTripActive = true;
|
|
581
|
+
if (mSensorFusion != null) mSensorFusion.setTripActive(true);
|
|
582
|
+
}
|
|
583
|
+
@Override public void onTripEnd(BackgroundLocation l, double distance, long durationMs) {
|
|
584
|
+
org.json.JSONObject extra = new org.json.JSONObject();
|
|
585
|
+
try { extra.put("distance", distance); extra.put("durationMs", durationMs); } catch (org.json.JSONException ignored) {}
|
|
586
|
+
attachDrivingEvent(l, "tripEnd", extra);
|
|
587
|
+
Bundle b = new Bundle();
|
|
588
|
+
b.putInt("action", MSG_ON_TRIP_END);
|
|
589
|
+
if (l != null) b.putParcelable("payload", l);
|
|
590
|
+
b.putDouble("distance", distance);
|
|
591
|
+
b.putLong("durationMs", durationMs);
|
|
592
|
+
broadcastMessage(b);
|
|
593
|
+
mDrivingTripActive = false;
|
|
594
|
+
if (mSensorFusion != null) mSensorFusion.setTripActive(false);
|
|
595
|
+
}
|
|
596
|
+
@Override public void onSpeeding(BackgroundLocation l, double speedKmh, double limitKmh) {
|
|
597
|
+
org.json.JSONObject extra = new org.json.JSONObject();
|
|
598
|
+
try { extra.put("speedKmh", speedKmh); extra.put("limitKmh", limitKmh); } catch (org.json.JSONException ignored) {}
|
|
599
|
+
attachDrivingEvent(l, "speeding", extra);
|
|
600
|
+
Bundle b = new Bundle();
|
|
601
|
+
b.putInt("action", MSG_ON_SPEEDING);
|
|
602
|
+
if (l != null) b.putParcelable("payload", l);
|
|
603
|
+
b.putDouble("speedKmh", speedKmh);
|
|
604
|
+
b.putDouble("limitKmh", limitKmh);
|
|
605
|
+
broadcastMessage(b);
|
|
606
|
+
}
|
|
607
|
+
@Override public void onProviderChange(String provider) {
|
|
608
|
+
// No location associated; buffer for next fix.
|
|
609
|
+
org.json.JSONObject ev = new org.json.JSONObject();
|
|
610
|
+
try { ev.put("type", "providerChange"); ev.put("provider", provider != null ? provider : ""); ev.put("time", System.currentTimeMillis()); } catch (org.json.JSONException ignored) {}
|
|
611
|
+
enqueuePendingDrivingEvent(ev);
|
|
612
|
+
Bundle b = new Bundle();
|
|
613
|
+
b.putInt("action", MSG_ON_PROVIDER_CHANGE);
|
|
614
|
+
b.putString("provider", provider != null ? provider : "");
|
|
615
|
+
broadcastMessage(b);
|
|
616
|
+
}
|
|
617
|
+
@Override public void onHardBrake(BackgroundLocation l, double decelMps2) {
|
|
618
|
+
org.json.JSONObject extra = new org.json.JSONObject();
|
|
619
|
+
try { extra.put("value", decelMps2); } catch (org.json.JSONException ignored) {}
|
|
620
|
+
attachDrivingEvent(l, "hardBrake", extra);
|
|
621
|
+
Bundle b = new Bundle();
|
|
622
|
+
b.putInt("action", MSG_ON_HARD_BRAKE);
|
|
623
|
+
if (l != null) b.putParcelable("payload", l);
|
|
624
|
+
b.putDouble("value", decelMps2);
|
|
625
|
+
broadcastMessage(b);
|
|
626
|
+
}
|
|
627
|
+
@Override public void onRapidAcceleration(BackgroundLocation l, double accelMps2) {
|
|
628
|
+
org.json.JSONObject extra = new org.json.JSONObject();
|
|
629
|
+
try { extra.put("value", accelMps2); } catch (org.json.JSONException ignored) {}
|
|
630
|
+
attachDrivingEvent(l, "rapidAcceleration", extra);
|
|
631
|
+
Bundle b = new Bundle();
|
|
632
|
+
b.putInt("action", MSG_ON_RAPID_ACCELERATION);
|
|
633
|
+
if (l != null) b.putParcelable("payload", l);
|
|
634
|
+
b.putDouble("value", accelMps2);
|
|
635
|
+
broadcastMessage(b);
|
|
636
|
+
}
|
|
637
|
+
@Override public void onSharpTurn(BackgroundLocation l, double degPerSec) {
|
|
638
|
+
org.json.JSONObject extra = new org.json.JSONObject();
|
|
639
|
+
try { extra.put("value", degPerSec); } catch (org.json.JSONException ignored) {}
|
|
640
|
+
attachDrivingEvent(l, "sharpTurn", extra);
|
|
641
|
+
Bundle b = new Bundle();
|
|
642
|
+
b.putInt("action", MSG_ON_SHARP_TURN);
|
|
643
|
+
if (l != null) b.putParcelable("payload", l);
|
|
644
|
+
b.putDouble("value", degPerSec);
|
|
645
|
+
broadcastMessage(b);
|
|
646
|
+
}
|
|
647
|
+
@Override public void onPossibleCrash(BackgroundLocation l, double velocityDropKmh) {
|
|
648
|
+
org.json.JSONObject extra = new org.json.JSONObject();
|
|
649
|
+
try { extra.put("value", velocityDropKmh); extra.put("source", "gps"); } catch (org.json.JSONException ignored) {}
|
|
650
|
+
attachDrivingEvent(l, "possibleCrash", extra);
|
|
651
|
+
Bundle b = new Bundle();
|
|
652
|
+
b.putInt("action", MSG_ON_POSSIBLE_CRASH);
|
|
653
|
+
if (l != null) b.putParcelable("payload", l);
|
|
654
|
+
b.putDouble("value", velocityDropKmh);
|
|
655
|
+
b.putString("source", "gps");
|
|
656
|
+
broadcastMessage(b);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
// Pass v4.1 thresholds from app config (with defaults from c).
|
|
660
|
+
com.marianhello.bgloc.Config.DrivingEventsOptions optsRef = mConfig.getDrivingEvents();
|
|
661
|
+
if (optsRef != null) {
|
|
662
|
+
c.hardBrakeMps2 = optsRef.hardBrakeMps2;
|
|
663
|
+
c.rapidAccelMps2 = optsRef.rapidAccelMps2;
|
|
664
|
+
c.sharpTurnDegPerSec = optsRef.sharpTurnDegPerSec;
|
|
665
|
+
c.crashImpactKmh = optsRef.crashImpactKmh;
|
|
666
|
+
c.crashWindowMs = optsRef.crashWindowMs;
|
|
667
|
+
}
|
|
668
|
+
mDrivingDetector.setConfig(c);
|
|
669
|
+
configureSensorFusion();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** v4.2 Phase 8: instantiate / reconfigure the real sensor-fusion detector. */
|
|
673
|
+
private void configureSensorFusion() {
|
|
674
|
+
if (mConfig == null) {
|
|
675
|
+
if (mSensorFusion != null) { mSensorFusion.stop(); mSensorFusion = null; }
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
com.marianhello.bgloc.Config.DrivingEventsOptions opts = mConfig.getDrivingEvents();
|
|
679
|
+
boolean wantSF = opts != null && opts.enabled && opts.sensorFusion;
|
|
680
|
+
if (!wantSF) {
|
|
681
|
+
if (mSensorFusion != null) { mSensorFusion.stop(); mSensorFusion = null; }
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
com.marianhello.bgloc.sensor.SensorFusionDetector.Listener l =
|
|
686
|
+
new com.marianhello.bgloc.sensor.SensorFusionDetector.Listener() {
|
|
687
|
+
@Override public void onSensorCrash(BackgroundLocation lastLocation, double impactG) {
|
|
688
|
+
// Buffer for next fix (sensor events fire async to GPS).
|
|
689
|
+
try {
|
|
690
|
+
org.json.JSONObject ev = new org.json.JSONObject();
|
|
691
|
+
ev.put("type", "possibleCrash");
|
|
692
|
+
ev.put("value", impactG);
|
|
693
|
+
ev.put("source", "sensor");
|
|
694
|
+
ev.put("time", System.currentTimeMillis());
|
|
695
|
+
enqueuePendingDrivingEvent(ev);
|
|
696
|
+
} catch (org.json.JSONException ignored) {}
|
|
697
|
+
Bundle b = new Bundle();
|
|
698
|
+
b.putInt("action", MSG_ON_POSSIBLE_CRASH);
|
|
699
|
+
if (lastLocation != null) b.putParcelable("payload", lastLocation);
|
|
700
|
+
b.putDouble("value", impactG);
|
|
701
|
+
b.putString("source", "sensor");
|
|
702
|
+
broadcastMessage(b);
|
|
703
|
+
}
|
|
704
|
+
@Override public void onPhoneUsageWhileDriving(BackgroundLocation lastLocation) {
|
|
705
|
+
try {
|
|
706
|
+
org.json.JSONObject ev = new org.json.JSONObject();
|
|
707
|
+
ev.put("type", "phoneUsageWhileDriving");
|
|
708
|
+
ev.put("time", System.currentTimeMillis());
|
|
709
|
+
enqueuePendingDrivingEvent(ev);
|
|
710
|
+
} catch (org.json.JSONException ignored) {}
|
|
711
|
+
Bundle b = new Bundle();
|
|
712
|
+
b.putInt("action", MSG_ON_PHONE_USAGE_WHILE_DRIVING);
|
|
713
|
+
if (lastLocation != null) b.putParcelable("payload", lastLocation);
|
|
714
|
+
broadcastMessage(b);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
if (mSensorFusion == null) {
|
|
719
|
+
mSensorFusion = new com.marianhello.bgloc.sensor.SensorFusionDetector(this, l);
|
|
720
|
+
}
|
|
721
|
+
com.marianhello.bgloc.sensor.SensorFusionDetector.Config sfc =
|
|
722
|
+
new com.marianhello.bgloc.sensor.SensorFusionDetector.Config();
|
|
723
|
+
sfc.enabled = true;
|
|
724
|
+
sfc.crashImpactG = opts.crashImpactG;
|
|
725
|
+
sfc.crashCooldownMs = opts.sensorCrashCooldownMs;
|
|
726
|
+
sfc.phoneUsageWindowMs = opts.phoneUsageWindowMs;
|
|
727
|
+
sfc.phoneUsageCooldownMs = opts.phoneUsageCooldownMs;
|
|
728
|
+
mSensorFusion.setConfig(sfc);
|
|
729
|
+
// v4.2 hot-reload: re-inject current tripActive state and last location so the
|
|
730
|
+
// sensor pipeline starts in the right mode (e.g. config arrives mid-trip).
|
|
731
|
+
mSensorFusion.setTripActive(mDrivingTripActive);
|
|
732
|
+
if (mLastReceivedLocation != null) {
|
|
733
|
+
mSensorFusion.setLastLocation(mLastReceivedLocation);
|
|
734
|
+
}
|
|
735
|
+
if (sIsRunning) mSensorFusion.start();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** v4.3 — append a {type, time, ...extra} entry to the location's events array. */
|
|
739
|
+
private void attachDrivingEvent(BackgroundLocation loc, String type, org.json.JSONObject extra) {
|
|
740
|
+
if (loc == null || type == null) return;
|
|
741
|
+
try {
|
|
742
|
+
org.json.JSONObject ev = new org.json.JSONObject();
|
|
743
|
+
ev.put("type", type);
|
|
744
|
+
ev.put("time", System.currentTimeMillis());
|
|
745
|
+
if (extra != null) {
|
|
746
|
+
java.util.Iterator<String> keys = extra.keys();
|
|
747
|
+
while (keys.hasNext()) {
|
|
748
|
+
String k = keys.next();
|
|
749
|
+
ev.put(k, extra.opt(k));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
loc.addDrivingEvent(ev);
|
|
753
|
+
} catch (org.json.JSONException ignored) {}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** v4.5.1 — acquire a short, time-bounded wake lock when wakeLockMode is 'posting'. */
|
|
757
|
+
private void acquireWakeLockForPosting() {
|
|
758
|
+
if (mWakeLock == null || mConfig == null) return;
|
|
759
|
+
String mode = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
|
|
760
|
+
if (!"posting".equals(mode)) return;
|
|
761
|
+
try {
|
|
762
|
+
// Bounded: SQLite write + HTTP POST should finish well within 30 s.
|
|
763
|
+
if (!mWakeLock.isHeld()) mWakeLock.acquire(30_000L);
|
|
764
|
+
} catch (Throwable ignored) { /* best-effort */ }
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** v4.4 — read current device battery via sticky broadcast and stamp it onto the location.
|
|
768
|
+
* No permission required. Sticky broadcast returns instantly without blocking.
|
|
769
|
+
* v4.4.1: route through the application context to bypass our own registerReceiver()
|
|
770
|
+
* override (which forces RECEIVER_NOT_EXPORTED + handler — incompatible with sticky-only reads). */
|
|
771
|
+
private void attachBatterySnapshot(BackgroundLocation loc) {
|
|
772
|
+
if (loc == null) return;
|
|
773
|
+
try {
|
|
774
|
+
android.content.IntentFilter filter = new android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED);
|
|
775
|
+
android.content.Intent batteryStatus = getApplicationContext().registerReceiver(null, filter);
|
|
776
|
+
if (batteryStatus == null) return;
|
|
777
|
+
int level = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1);
|
|
778
|
+
int scale = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1);
|
|
779
|
+
if (level >= 0 && scale > 0) {
|
|
780
|
+
loc.setBatteryLevel((int) Math.round(level * 100.0 / scale));
|
|
781
|
+
}
|
|
782
|
+
int status = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_STATUS, -1);
|
|
783
|
+
boolean charging = (status == android.os.BatteryManager.BATTERY_STATUS_CHARGING
|
|
784
|
+
|| status == android.os.BatteryManager.BATTERY_STATUS_FULL);
|
|
785
|
+
loc.setCharging(charging);
|
|
786
|
+
} catch (Throwable ignored) { /* best-effort; never fail the fix */ }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** v4.3 — drain pending events (those fired without a simultaneous fix) onto this location.
|
|
790
|
+
* v4.4.1: drop entries older than PENDING_DRIVING_EVENTS_TTL_MS so we don't anexar an event
|
|
791
|
+
* whose context (location, speed, etc.) is no longer relevant. */
|
|
792
|
+
private void flushPendingDrivingEvents(BackgroundLocation loc) {
|
|
793
|
+
if (loc == null) return;
|
|
794
|
+
long now = System.currentTimeMillis();
|
|
795
|
+
synchronized (mPendingDrivingEvents) {
|
|
796
|
+
int n = mPendingDrivingEvents.length();
|
|
797
|
+
if (n == 0) return;
|
|
798
|
+
for (int i = 0; i < n; i++) {
|
|
799
|
+
org.json.JSONObject ev = mPendingDrivingEvents.optJSONObject(i);
|
|
800
|
+
if (ev == null) continue;
|
|
801
|
+
long t = ev.optLong("time", now);
|
|
802
|
+
if (now - t <= PENDING_DRIVING_EVENTS_TTL_MS) {
|
|
803
|
+
loc.addDrivingEvent(ev);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
for (int i = n - 1; i >= 0; i--) mPendingDrivingEvents.remove(i);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** v4.4.1 — append to pending events with cap (oldest evicted). */
|
|
811
|
+
private void enqueuePendingDrivingEvent(org.json.JSONObject ev) {
|
|
812
|
+
if (ev == null) return;
|
|
813
|
+
synchronized (mPendingDrivingEvents) {
|
|
814
|
+
while (mPendingDrivingEvents.length() >= PENDING_DRIVING_EVENTS_MAX) {
|
|
815
|
+
mPendingDrivingEvents.remove(0);
|
|
816
|
+
}
|
|
817
|
+
mPendingDrivingEvents.put(ev);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@Override
|
|
822
|
+
public synchronized void startForegroundService() {
|
|
823
|
+
start();
|
|
824
|
+
startForeground();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
@Override
|
|
828
|
+
public synchronized void stop() {
|
|
829
|
+
if (!sIsRunning) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
mMainHandler.removeCallbacks(mWatchdogRunnable);
|
|
834
|
+
mMainHandler.removeCallbacks(mNotificationUpdateRunnable);
|
|
835
|
+
|
|
836
|
+
if (mWakeLock != null && mWakeLock.isHeld()) {
|
|
837
|
+
try {
|
|
838
|
+
mWakeLock.release();
|
|
839
|
+
logger.debug("Wake lock released");
|
|
840
|
+
} catch (Exception e) {
|
|
841
|
+
logger.warn("Wake lock release failed", e);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (mProvider != null) {
|
|
846
|
+
mProvider.onStop();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
mIsInForeground = false;
|
|
850
|
+
stopForeground(true);
|
|
851
|
+
stopSelf();
|
|
852
|
+
|
|
853
|
+
// v3.5 Phase 4: stop heartbeat scheduler.
|
|
854
|
+
cancelHeartbeat();
|
|
855
|
+
// v4.0 Phase 6: reset driver-insights state machine.
|
|
856
|
+
if (mDrivingDetector != null) mDrivingDetector.reset();
|
|
857
|
+
// v4.2 Phase 8: stop sensor fusion sampling.
|
|
858
|
+
mDrivingTripActive = false;
|
|
859
|
+
if (mSensorFusion != null) {
|
|
860
|
+
mSensorFusion.setTripActive(false);
|
|
861
|
+
mSensorFusion.stop();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
broadcastMessage(MSG_ON_SERVICE_STOPPED);
|
|
865
|
+
sIsRunning = false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Returns true if the app has at least one of the location runtime permissions.
|
|
870
|
+
* Required before starting a location foreground service on API 34+.
|
|
871
|
+
*/
|
|
872
|
+
private boolean hasLocationPermission() {
|
|
873
|
+
// v4.5.1 — ContextCompat handles API < 23 (permissions granted at install time).
|
|
874
|
+
return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
875
|
+
|| ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Reads this service's foregroundServiceType from the merged AndroidManifest (API 34+).
|
|
880
|
+
* Uses ComponentInfoFlags.of(0) (not GET_META_DATA) so getServiceInfo returns complete ServiceInfo.
|
|
881
|
+
* Returns the real value; never invents a hardcoded type. If unknown, returns 0 so callers must not call startForeground.
|
|
882
|
+
* Requires compileSdk 33+ (ComponentInfoFlags); 34+ for ServiceInfo.foregroundServiceType.
|
|
883
|
+
*/
|
|
884
|
+
private int getManifestForegroundServiceType() {
|
|
885
|
+
if (Build.VERSION.SDK_INT < 34) return 0;
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
ComponentName cn = new ComponentName(this, LocationServiceImpl.class);
|
|
889
|
+
|
|
890
|
+
ServiceInfo si;
|
|
891
|
+
if (Build.VERSION.SDK_INT >= 33) {
|
|
892
|
+
si = getPackageManager().getServiceInfo(
|
|
893
|
+
cn,
|
|
894
|
+
PackageManager.ComponentInfoFlags.of(0)
|
|
895
|
+
);
|
|
896
|
+
} else {
|
|
897
|
+
si = getPackageManager().getServiceInfo(cn, 0);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
int t = getForegroundServiceTypeFromServiceInfo(si);
|
|
901
|
+
logger.info("Manifest foregroundServiceType=0x{}", Integer.toHexString(t));
|
|
902
|
+
return t;
|
|
903
|
+
} catch (Throwable e) {
|
|
904
|
+
logger.warn("getManifestForegroundServiceType failed: {}", e.getMessage());
|
|
905
|
+
return 0;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** Read foregroundServiceType from ServiceInfo (field exists in API 34; use reflection to compile with compileSdk 33). */
|
|
910
|
+
private int getForegroundServiceTypeFromServiceInfo(ServiceInfo si) {
|
|
911
|
+
try {
|
|
912
|
+
java.lang.reflect.Field f = ServiceInfo.class.getField("foregroundServiceType");
|
|
913
|
+
Object v = f.get(si);
|
|
914
|
+
return (v instanceof Integer) ? (Integer) v : 0;
|
|
915
|
+
} catch (Throwable ignored) {
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
@Override
|
|
921
|
+
public void startForeground() {
|
|
922
|
+
if (sIsRunning && !mIsInForeground) {
|
|
923
|
+
if (!hasLocationPermission()) {
|
|
924
|
+
logger.warn("Cannot start foreground: location permission not granted");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
Config config = getConfig();
|
|
928
|
+
String contentText = buildNotificationContentText(config);
|
|
929
|
+
Notification notification = new NotificationHelper.NotificationFactory(this).getNotification(
|
|
930
|
+
config.getNotificationTitle(),
|
|
931
|
+
contentText,
|
|
932
|
+
config.getLargeNotificationIcon(),
|
|
933
|
+
config.getSmallNotificationIcon(),
|
|
934
|
+
config.getNotificationIconColor());
|
|
935
|
+
|
|
936
|
+
if (mProvider != null) {
|
|
937
|
+
mProvider.onCommand(LocationProvider.CMD_SWITCH_MODE,
|
|
938
|
+
LocationProvider.FOREGROUND_MODE);
|
|
939
|
+
}
|
|
940
|
+
// Android 14+ (API 34): type is required. Android 12-13 (API 31-33): type accepted (preferred).
|
|
941
|
+
// FOREGROUND_SERVICE_TYPE_LOCATION = 0x00000008. Resolve from merged manifest first;
|
|
942
|
+
// if reflection fails or manifest merge missed the attribute, fall back to LOCATION
|
|
943
|
+
// hardcoded so the FGS still promotes (otherwise: no notification, no background tracking).
|
|
944
|
+
try {
|
|
945
|
+
if (Build.VERSION.SDK_INT >= 30) {
|
|
946
|
+
int type = getManifestForegroundServiceType();
|
|
947
|
+
if (type == 0) {
|
|
948
|
+
// Defensive fallback: every consumer of this plugin requires location FGS.
|
|
949
|
+
// Logging at warn so the failure is visible without breaking the service.
|
|
950
|
+
logger.warn("Manifest foregroundServiceType unreadable; defaulting to LOCATION (0x8). "
|
|
951
|
+
+ "Verify merged AndroidManifest has foregroundServiceType=\"location\"." );
|
|
952
|
+
type = 0x00000008; // ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
|
953
|
+
}
|
|
954
|
+
super.startForeground(NOTIFICATION_ID, notification, type);
|
|
955
|
+
} else {
|
|
956
|
+
super.startForeground(NOTIFICATION_ID, notification);
|
|
957
|
+
}
|
|
958
|
+
} catch (Throwable t) {
|
|
959
|
+
logger.error("startForeground threw {}; retrying without type", t.getMessage());
|
|
960
|
+
try {
|
|
961
|
+
super.startForeground(NOTIFICATION_ID, notification);
|
|
962
|
+
} catch (Throwable t2) {
|
|
963
|
+
logger.error("startForeground retry failed: {}", t2.getMessage());
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
mIsInForeground = true;
|
|
968
|
+
scheduleNotificationUpdater();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
@Override
|
|
973
|
+
public synchronized void stopForeground() {
|
|
974
|
+
if (sIsRunning && mIsInForeground) {
|
|
975
|
+
mMainHandler.removeCallbacks(mNotificationUpdateRunnable);
|
|
976
|
+
stopForeground(true);
|
|
977
|
+
if (mProvider != null) {
|
|
978
|
+
mProvider.onCommand(LocationProvider.CMD_SWITCH_MODE,
|
|
979
|
+
LocationProvider.BACKGROUND_MODE);
|
|
980
|
+
}
|
|
981
|
+
mIsInForeground = false;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** Resource names for optional app-localized notification labels (showTime / showDistance). */
|
|
986
|
+
private static final String RES_NOTIFICATION_TIME_LABEL = "plugin_bgloc_notification_time_label";
|
|
987
|
+
private static final String RES_NOTIFICATION_DISTANCE_LABEL = "plugin_bgloc_notification_distance_label";
|
|
988
|
+
|
|
989
|
+
private String getNotificationLabel(String resourceName, String defaultValue) {
|
|
990
|
+
Context app = getApplicationContext();
|
|
991
|
+
int id = app.getResources().getIdentifier(resourceName, "string", app.getPackageName());
|
|
992
|
+
return (id != 0) ? app.getString(id) : defaultValue;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private String buildNotificationContentText(Config config) {
|
|
996
|
+
String base = config.getNotificationText() != null ? config.getNotificationText() : "ENABLED";
|
|
997
|
+
if (Boolean.TRUE.equals(config.getShowTime())) {
|
|
998
|
+
String timeLabel = getNotificationLabel(RES_NOTIFICATION_TIME_LABEL, "Time");
|
|
999
|
+
base += "\n" + timeLabel + ": " + formatElapsed(mSessionStartTime);
|
|
1000
|
+
}
|
|
1001
|
+
if (Boolean.TRUE.equals(config.getShowDistance())) {
|
|
1002
|
+
String distanceLabel = getNotificationLabel(RES_NOTIFICATION_DISTANCE_LABEL, "Distance");
|
|
1003
|
+
base += "\n" + distanceLabel + ": " + formatDistance(mSessionDistanceMeters);
|
|
1004
|
+
}
|
|
1005
|
+
return base;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
private static String formatElapsed(long startTimeMs) {
|
|
1009
|
+
long elapsed = Math.max(0L, System.currentTimeMillis() - startTimeMs);
|
|
1010
|
+
long s = (elapsed / 1000L) % 60L;
|
|
1011
|
+
long m = (elapsed / 60000L) % 60L;
|
|
1012
|
+
long h = elapsed / 3600000L;
|
|
1013
|
+
return String.format(Locale.US, "%02d:%02d:%02d", h, m, s);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private static String formatDistance(double meters) {
|
|
1017
|
+
return String.format(Locale.US, "%.2f km", meters / 1000.0);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private void updateForegroundNotification() {
|
|
1021
|
+
if (!sIsRunning || !mIsInForeground || mConfig == null) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
String contentText = buildNotificationContentText(mConfig);
|
|
1025
|
+
Notification notification = new NotificationHelper.NotificationFactory(this).getNotification(
|
|
1026
|
+
mConfig.getNotificationTitle(),
|
|
1027
|
+
contentText,
|
|
1028
|
+
mConfig.getLargeNotificationIcon(),
|
|
1029
|
+
mConfig.getSmallNotificationIcon(),
|
|
1030
|
+
mConfig.getNotificationIconColor());
|
|
1031
|
+
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
1032
|
+
if (nm != null) {
|
|
1033
|
+
nm.notify(NOTIFICATION_ID, notification);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private void scheduleNotificationUpdater() {
|
|
1038
|
+
mMainHandler.removeCallbacks(mNotificationUpdateRunnable);
|
|
1039
|
+
if (mConfig == null) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
boolean showTime = Boolean.TRUE.equals(mConfig.getShowTime());
|
|
1043
|
+
boolean showDistance = Boolean.TRUE.equals(mConfig.getShowDistance());
|
|
1044
|
+
if ((showTime || showDistance) && sIsRunning && mIsInForeground) {
|
|
1045
|
+
mMainHandler.postDelayed(mNotificationUpdateRunnable, NOTIFICATION_UPDATE_INTERVAL_MS);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
@Override
|
|
1050
|
+
public synchronized void configure(Config config) {
|
|
1051
|
+
if (mConfig == null) {
|
|
1052
|
+
mConfig = config;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
final Config currentConfig = mConfig;
|
|
1057
|
+
mConfig = config;
|
|
1058
|
+
|
|
1059
|
+
mPostLocationTask.setConfig(mConfig);
|
|
1060
|
+
|
|
1061
|
+
ThreadUtils.runOnUiThread(new Runnable() {
|
|
1062
|
+
@Override
|
|
1063
|
+
public void run() {
|
|
1064
|
+
if (sIsRunning) {
|
|
1065
|
+
if (currentConfig.getStartForeground() == true && mConfig.getStartForeground() == false) {
|
|
1066
|
+
stopForeground();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (mConfig.getStartForeground() == true) {
|
|
1070
|
+
if (currentConfig.getStartForeground() == false) {
|
|
1071
|
+
// was not running in foreground, so start in foreground
|
|
1072
|
+
startForeground();
|
|
1073
|
+
} else {
|
|
1074
|
+
// was running in foreground, so just update existing notification
|
|
1075
|
+
String contentText = buildNotificationContentText(mConfig);
|
|
1076
|
+
Notification notification = new NotificationHelper.NotificationFactory(LocationServiceImpl.this).getNotification(
|
|
1077
|
+
mConfig.getNotificationTitle(),
|
|
1078
|
+
contentText,
|
|
1079
|
+
mConfig.getLargeNotificationIcon(),
|
|
1080
|
+
mConfig.getSmallNotificationIcon(),
|
|
1081
|
+
mConfig.getNotificationIconColor());
|
|
1082
|
+
|
|
1083
|
+
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
1084
|
+
notificationManager.notify(NOTIFICATION_ID, notification);
|
|
1085
|
+
scheduleNotificationUpdater();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (currentConfig.getLocationProvider() != mConfig.getLocationProvider()) {
|
|
1091
|
+
boolean shouldStart = mProvider.isStarted();
|
|
1092
|
+
mProvider.onDestroy();
|
|
1093
|
+
LocationProviderFactory spf = new LocationProviderFactory(LocationServiceImpl.this);
|
|
1094
|
+
mProvider = spf.getInstance(mConfig.getLocationProvider());
|
|
1095
|
+
mProvider.setDelegate(LocationServiceImpl.this);
|
|
1096
|
+
mProvider.onCreate();
|
|
1097
|
+
mProvider.onConfigure(mConfig);
|
|
1098
|
+
if (shouldStart) {
|
|
1099
|
+
mProvider.onStart();
|
|
1100
|
+
}
|
|
1101
|
+
} else {
|
|
1102
|
+
mProvider.onConfigure(mConfig);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// v4.1: re-evaluate hot-reload features when config changes while service is running.
|
|
1106
|
+
if (sIsRunning) {
|
|
1107
|
+
Integer prevHb = currentConfig.getHeartbeatInterval();
|
|
1108
|
+
Integer newHb = mConfig.getHeartbeatInterval();
|
|
1109
|
+
if (prevHb == null) prevHb = 0;
|
|
1110
|
+
if (newHb == null) newHb = 0;
|
|
1111
|
+
if (!prevHb.equals(newHb)) {
|
|
1112
|
+
scheduleHeartbeat(); // cancels and reschedules with the new interval (or stops if 0)
|
|
1113
|
+
}
|
|
1114
|
+
// Driver-insights detector: rebuild if the config dict changed.
|
|
1115
|
+
Config.DrivingEventsOptions prevDe = currentConfig.getDrivingEvents();
|
|
1116
|
+
Config.DrivingEventsOptions newDe = mConfig.getDrivingEvents();
|
|
1117
|
+
if (!equalsDrivingEvents(prevDe, newDe)) {
|
|
1118
|
+
configureDrivingDetector();
|
|
1119
|
+
}
|
|
1120
|
+
// v4.5.1 — hot-reload wakeLockMode: when transitioning between always /
|
|
1121
|
+
// posting / none, the existing permanent lock (if any) must be released,
|
|
1122
|
+
// or a new permanent lock acquired. Without this, switching mode at runtime
|
|
1123
|
+
// either leaked CPU or left the service running without the requested lock.
|
|
1124
|
+
String prevWl = currentConfig.getWakeLockMode() != null ? currentConfig.getWakeLockMode() : "posting";
|
|
1125
|
+
String newWl = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
|
|
1126
|
+
if (!prevWl.equals(newWl) && mWakeLock != null) {
|
|
1127
|
+
if ("always".equals(newWl)) {
|
|
1128
|
+
if (!mWakeLock.isHeld()) {
|
|
1129
|
+
try { mWakeLock.acquire(); logger.debug("Wake lock acquired (hot-reload → always)"); }
|
|
1130
|
+
catch (Throwable t) { logger.warn("Wake lock acquire failed", t); }
|
|
1131
|
+
}
|
|
1132
|
+
} else {
|
|
1133
|
+
// 'posting' or 'none' — release any permanent lock; per-fix lock continues to work via acquireWakeLockForPosting().
|
|
1134
|
+
if (mWakeLock.isHeld()) {
|
|
1135
|
+
try { mWakeLock.release(); logger.debug("Wake lock released (hot-reload → {})", newWl); }
|
|
1136
|
+
catch (Throwable t) { logger.warn("Wake lock release failed", t); }
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** Shallow value equality for DrivingEventsOptions; avoids needless detector rebuilds. */
|
|
1146
|
+
private static boolean equalsDrivingEvents(Config.DrivingEventsOptions a, Config.DrivingEventsOptions b) {
|
|
1147
|
+
if (a == b) return true;
|
|
1148
|
+
if (a == null || b == null) return false;
|
|
1149
|
+
return a.enabled == b.enabled
|
|
1150
|
+
&& a.speedLimitKmh == b.speedLimitKmh
|
|
1151
|
+
&& a.minMovingSpeedMps == b.minMovingSpeedMps
|
|
1152
|
+
&& a.stoppedDurationMs == b.stoppedDurationMs
|
|
1153
|
+
&& a.minTripSpeedMps == b.minTripSpeedMps
|
|
1154
|
+
&& a.minTripDurationMs == b.minTripDurationMs
|
|
1155
|
+
&& a.hardBrakeMps2 == b.hardBrakeMps2
|
|
1156
|
+
&& a.rapidAccelMps2 == b.rapidAccelMps2
|
|
1157
|
+
&& a.sharpTurnDegPerSec == b.sharpTurnDegPerSec
|
|
1158
|
+
&& a.crashImpactKmh == b.crashImpactKmh
|
|
1159
|
+
&& a.crashWindowMs == b.crashWindowMs
|
|
1160
|
+
&& a.sensorFusion == b.sensorFusion
|
|
1161
|
+
&& a.crashImpactG == b.crashImpactG
|
|
1162
|
+
&& a.sensorCrashCooldownMs == b.sensorCrashCooldownMs
|
|
1163
|
+
&& a.phoneUsageWindowMs == b.phoneUsageWindowMs
|
|
1164
|
+
&& a.phoneUsageCooldownMs == b.phoneUsageCooldownMs;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
@Override
|
|
1168
|
+
public synchronized void registerHeadlessTask(String taskRunnerClass) {
|
|
1169
|
+
logger.debug("Registering headless task");
|
|
1170
|
+
mHeadlessTaskRunnerClass = taskRunnerClass;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
@Override
|
|
1174
|
+
public synchronized void startHeadlessTask() {
|
|
1175
|
+
if (mHeadlessTaskRunnerClass != null) {
|
|
1176
|
+
TaskRunnerFactory trf = new TaskRunnerFactory();
|
|
1177
|
+
try {
|
|
1178
|
+
mHeadlessTaskRunner = trf.getTaskRunner(mHeadlessTaskRunnerClass);
|
|
1179
|
+
((AbstractTaskRunner) mHeadlessTaskRunner).setContext(this);
|
|
1180
|
+
} catch (Exception e) {
|
|
1181
|
+
logger.error("Headless task start failed: {}", e.getMessage());
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
@Override
|
|
1187
|
+
public synchronized void stopHeadlessTask() {
|
|
1188
|
+
mHeadlessTaskRunner = null;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
@Override
|
|
1192
|
+
public synchronized void executeProviderCommand(final int command, final int arg1) {
|
|
1193
|
+
if (mProvider == null) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
ThreadUtils.runOnUiThread(new Runnable() {
|
|
1198
|
+
@Override
|
|
1199
|
+
public void run() {
|
|
1200
|
+
mProvider.onCommand(command, arg1);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
@Override
|
|
1206
|
+
public void onLocation(BackgroundLocation location) {
|
|
1207
|
+
// v4.5.1: in 'posting' wake-lock mode, hold the CPU briefly so SQLite writes + HTTP
|
|
1208
|
+
// POST finish before the system returns to deep sleep. 30s ceiling — plenty for a fix.
|
|
1209
|
+
acquireWakeLockForPosting();
|
|
1210
|
+
mLastLocationTime = System.currentTimeMillis();
|
|
1211
|
+
mLastReceivedLocation = location;
|
|
1212
|
+
sLastReceivedLocation = location;
|
|
1213
|
+
|
|
1214
|
+
// v4.0 Phase 6: feed the driver-insights state machine on the *raw* location so speed/bearing
|
|
1215
|
+
// come straight from the sensors. Listener attaches events to this same instance.
|
|
1216
|
+
if (mDrivingDetector != null) {
|
|
1217
|
+
mDrivingDetector.onLocation(location);
|
|
1218
|
+
}
|
|
1219
|
+
// v4.2 Phase 8: keep sensor pipeline aware of the latest raw fix.
|
|
1220
|
+
if (mSensorFusion != null) {
|
|
1221
|
+
mSensorFusion.setLastLocation(location);
|
|
1222
|
+
}
|
|
1223
|
+
if (Boolean.TRUE.equals(mConfig != null ? mConfig.getShowDistance() : null)) {
|
|
1224
|
+
double lat = location.getLatitude();
|
|
1225
|
+
double lon = location.getLongitude();
|
|
1226
|
+
if (mHasLastLocation) {
|
|
1227
|
+
float[] dist = new float[1];
|
|
1228
|
+
Location.distanceBetween(mLastLat, mLastLon, lat, lon, dist);
|
|
1229
|
+
mSessionDistanceMeters += (double) dist[0];
|
|
1230
|
+
}
|
|
1231
|
+
mLastLat = lat;
|
|
1232
|
+
mLastLon = lon;
|
|
1233
|
+
mHasLastLocation = true;
|
|
1234
|
+
if (mIsInForeground && mConfig != null && (Boolean.TRUE.equals(mConfig.getShowTime()) || Boolean.TRUE.equals(mConfig.getShowDistance()))) {
|
|
1235
|
+
mMainHandler.post(new Runnable() {
|
|
1236
|
+
@Override
|
|
1237
|
+
public void run() {
|
|
1238
|
+
updateForegroundNotification();
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
logger.debug("New location {}", location.toString());
|
|
1244
|
+
|
|
1245
|
+
// v4.5.1 — events were attached to the RAW location above (so detector heuristics see real
|
|
1246
|
+
// speed/bearing). If transformLocation() returns a NEW instance, we'd lose those events
|
|
1247
|
+
// and the battery snapshot. Solution: copy them across to the transformed instance below.
|
|
1248
|
+
org.json.JSONArray rawEvents = location.getDrivingEvents();
|
|
1249
|
+
Integer rawBatteryLevel = null;
|
|
1250
|
+
Boolean rawIsCharging = null;
|
|
1251
|
+
|
|
1252
|
+
location = transformLocation(location);
|
|
1253
|
+
if (location == null) {
|
|
1254
|
+
logger.debug("Skipping location as requested by the locationTransform");
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Re-attach events to the transformed location if the transform produced a new instance.
|
|
1259
|
+
if (rawEvents != null && rawEvents.length() > 0 && location.getDrivingEvents() != rawEvents) {
|
|
1260
|
+
try {
|
|
1261
|
+
org.json.JSONArray copy = new org.json.JSONArray(rawEvents.toString());
|
|
1262
|
+
for (int i = 0; i < copy.length(); i++) {
|
|
1263
|
+
org.json.JSONObject ev = copy.optJSONObject(i);
|
|
1264
|
+
if (ev != null) location.addDrivingEvent(ev);
|
|
1265
|
+
}
|
|
1266
|
+
} catch (org.json.JSONException ignored) {}
|
|
1267
|
+
}
|
|
1268
|
+
// v4.3: drain pending events (providerChange/sensor crash/phone usage) onto the post-transform
|
|
1269
|
+
// instance so they always reach the backend.
|
|
1270
|
+
flushPendingDrivingEvents(location);
|
|
1271
|
+
// v4.4: stamp device battery snapshot onto the *transformed* location so it survives any
|
|
1272
|
+
// user-supplied locationTransform that creates a new instance.
|
|
1273
|
+
if (mConfig == null || !Boolean.FALSE.equals(mConfig.getIncludeBattery())) {
|
|
1274
|
+
attachBatterySnapshot(location);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
Bundle bundle = new Bundle();
|
|
1278
|
+
bundle.putInt("action", MSG_ON_LOCATION);
|
|
1279
|
+
bundle.putParcelable("payload", location);
|
|
1280
|
+
broadcastMessage(bundle);
|
|
1281
|
+
|
|
1282
|
+
runHeadlessTask(new LocationTask(location) {
|
|
1283
|
+
@Override
|
|
1284
|
+
public void onError(String errorMessage) {
|
|
1285
|
+
logger.error("Location task error: {}", errorMessage);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
@Override
|
|
1289
|
+
public void onResult(String value) {
|
|
1290
|
+
logger.debug("Location task result: {}", value);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
postLocation(location);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
@Override
|
|
1298
|
+
public void onStationary(BackgroundLocation location) {
|
|
1299
|
+
logger.debug("New stationary {}", location.toString());
|
|
1300
|
+
|
|
1301
|
+
location = transformLocation(location);
|
|
1302
|
+
if (location == null) {
|
|
1303
|
+
logger.debug("Skipping location as requested by the locationTransform");
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
// v4.5.1 — same enrichment as regular fixes: drain pending events and stamp battery.
|
|
1307
|
+
flushPendingDrivingEvents(location);
|
|
1308
|
+
if (mConfig == null || !Boolean.FALSE.equals(mConfig.getIncludeBattery())) {
|
|
1309
|
+
attachBatterySnapshot(location);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
Bundle bundle = new Bundle();
|
|
1313
|
+
bundle.putInt("action", MSG_ON_STATIONARY);
|
|
1314
|
+
bundle.putParcelable("payload", location);
|
|
1315
|
+
broadcastMessage(bundle);
|
|
1316
|
+
|
|
1317
|
+
runHeadlessTask(new StationaryTask(location){
|
|
1318
|
+
@Override
|
|
1319
|
+
public void onError(String errorMessage) {
|
|
1320
|
+
logger.error("Stationary task error: {}", errorMessage);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
@Override
|
|
1324
|
+
public void onResult(String value) {
|
|
1325
|
+
logger.debug("Stationary task result: {}", value);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
postLocation(location);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
@Override
|
|
1333
|
+
public void onActivity(BackgroundActivity activity) {
|
|
1334
|
+
logger.debug("New activity {}", activity.toString());
|
|
1335
|
+
|
|
1336
|
+
Bundle bundle = new Bundle();
|
|
1337
|
+
bundle.putInt("action", MSG_ON_ACTIVITY);
|
|
1338
|
+
bundle.putParcelable("payload", activity);
|
|
1339
|
+
broadcastMessage(bundle);
|
|
1340
|
+
|
|
1341
|
+
runHeadlessTask(new ActivityTask(activity){
|
|
1342
|
+
@Override
|
|
1343
|
+
public void onError(String errorMessage) {
|
|
1344
|
+
logger.error("Activity task error: {}", errorMessage);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
@Override
|
|
1348
|
+
public void onResult(String value) {
|
|
1349
|
+
logger.debug("Activity task result: {}", value);
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
@Override
|
|
1355
|
+
public void onError(PluginException error) {
|
|
1356
|
+
Bundle bundle = new Bundle();
|
|
1357
|
+
bundle.putInt("action", MSG_ON_ERROR);
|
|
1358
|
+
bundle.putBundle("payload", error.toBundle());
|
|
1359
|
+
broadcastMessage(bundle);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
private void broadcastMessage(int msgId) {
|
|
1363
|
+
Bundle bundle = new Bundle();
|
|
1364
|
+
bundle.putInt("action", msgId);
|
|
1365
|
+
broadcastMessage(bundle);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
private void broadcastMessage(Bundle bundle) {
|
|
1369
|
+
Intent intent = new Intent(ACTION_BROADCAST);
|
|
1370
|
+
intent.putExtras(bundle);
|
|
1371
|
+
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** v3.5 Phase 4: schedule periodic heartbeat broadcasts using {@link Config#getHeartbeatInterval()}. */
|
|
1375
|
+
private void scheduleHeartbeat() {
|
|
1376
|
+
cancelHeartbeat();
|
|
1377
|
+
if (mConfig == null) return;
|
|
1378
|
+
Integer interval = mConfig.getHeartbeatInterval();
|
|
1379
|
+
if (interval == null || interval <= 0) return;
|
|
1380
|
+
logger.debug("Scheduling heartbeat every {} ms", interval);
|
|
1381
|
+
mHeartbeatExecutor = java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
|
|
1382
|
+
mHeartbeatTask = mHeartbeatExecutor.scheduleAtFixedRate(new Runnable() {
|
|
1383
|
+
@Override public void run() {
|
|
1384
|
+
try {
|
|
1385
|
+
Bundle b = new Bundle();
|
|
1386
|
+
b.putInt("action", MSG_ON_HEARTBEAT);
|
|
1387
|
+
BackgroundLocation last = mLastReceivedLocation;
|
|
1388
|
+
if (last != null) b.putParcelable("payload", last);
|
|
1389
|
+
broadcastMessage(b);
|
|
1390
|
+
} catch (Throwable t) {
|
|
1391
|
+
logger.warn("Heartbeat tick failed: {}", t.getMessage());
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}, interval, interval, java.util.concurrent.TimeUnit.MILLISECONDS);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
private void cancelHeartbeat() {
|
|
1398
|
+
if (mHeartbeatTask != null) {
|
|
1399
|
+
mHeartbeatTask.cancel(false);
|
|
1400
|
+
mHeartbeatTask = null;
|
|
1401
|
+
}
|
|
1402
|
+
if (mHeartbeatExecutor != null) {
|
|
1403
|
+
mHeartbeatExecutor.shutdownNow();
|
|
1404
|
+
mHeartbeatExecutor = null;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
@Override
|
|
1409
|
+
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
|
1410
|
+
// v4.5.1 — RECEIVER_NOT_EXPORTED flag is required on Android 13+ (API 33) for non-system
|
|
1411
|
+
// broadcasts and the 5-arg overload exists only from API 26. Guard for older OSs.
|
|
1412
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1413
|
+
return super.registerReceiver(receiver, filter, null, mServiceHandler, Context.RECEIVER_NOT_EXPORTED);
|
|
1414
|
+
}
|
|
1415
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1416
|
+
return super.registerReceiver(receiver, filter, null, mServiceHandler);
|
|
1417
|
+
}
|
|
1418
|
+
return super.registerReceiver(receiver, filter);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
@Override
|
|
1422
|
+
public void unregisterReceiver(BroadcastReceiver receiver) {
|
|
1423
|
+
try {
|
|
1424
|
+
super.unregisterReceiver(receiver);
|
|
1425
|
+
} catch (IllegalArgumentException ex) {
|
|
1426
|
+
// if was not registered ignore exception
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
public Config getConfig() {
|
|
1431
|
+
Config config = mConfig;
|
|
1432
|
+
if (config == null) {
|
|
1433
|
+
ConfigurationDAO dao = DAOFactory.createConfigurationDAO(this);
|
|
1434
|
+
try {
|
|
1435
|
+
config = dao.retrieveConfiguration();
|
|
1436
|
+
} catch (JSONException e) {
|
|
1437
|
+
logger.error("Config exception: {}", e.getMessage());
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (config == null) {
|
|
1442
|
+
config = Config.getDefault();
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
mConfig = config;
|
|
1446
|
+
return mConfig;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
public static void setLocationProviderFactory(LocationProviderFactory factory) {
|
|
1450
|
+
sLocationProviderFactory = factory;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private void runHeadlessTask(Task task) {
|
|
1454
|
+
if (mHeadlessTaskRunner == null) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
logger.debug("Running headless task: {}", task);
|
|
1459
|
+
mHeadlessTaskRunner.runTask(task);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Class used for the client Binder. Since this service runs in the same process as its
|
|
1464
|
+
* clients, we don't need to deal with IPC.
|
|
1465
|
+
*/
|
|
1466
|
+
public class LocalBinder extends Binder {
|
|
1467
|
+
public LocationServiceImpl getService() {
|
|
1468
|
+
return LocationServiceImpl.this;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private BackgroundLocation transformLocation(BackgroundLocation location) {
|
|
1473
|
+
if (sLocationTransform != null) {
|
|
1474
|
+
return sLocationTransform.transformLocationBeforeCommit(this, location);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return location;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private void postLocation(BackgroundLocation location) {
|
|
1481
|
+
mPostLocationTask.add(location);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
public void handleRequestedAbortUpdates() {
|
|
1485
|
+
broadcastMessage(MSG_ON_ABORT_REQUESTED);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
public void handleHttpAuthorizationUpdates() {
|
|
1489
|
+
broadcastMessage(MSG_ON_HTTP_AUTHORIZATION);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Broadcast receiver which detects connectivity change condition
|
|
1494
|
+
*/
|
|
1495
|
+
private BroadcastReceiver connectivityChangeReceiver = new BroadcastReceiver() {
|
|
1496
|
+
@Override
|
|
1497
|
+
public void onReceive(Context context, Intent intent) {
|
|
1498
|
+
boolean hasConnectivity = isNetworkAvailable();
|
|
1499
|
+
mPostLocationTask.setHasConnectivity(hasConnectivity);
|
|
1500
|
+
logger.info("Network condition changed has connectivity: {}", hasConnectivity);
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
private boolean isNetworkAvailable() {
|
|
1505
|
+
ConnectivityManager cm =
|
|
1506
|
+
(ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
1507
|
+
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
|
1508
|
+
return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
public long getServiceId() {
|
|
1512
|
+
return mServiceId;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
public boolean isBound() {
|
|
1516
|
+
LocationServiceInfo info = new LocationServiceInfoImpl(this);
|
|
1517
|
+
return info.isBound();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
public static boolean isRunning() {
|
|
1521
|
+
return sIsRunning;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
public static void setLocationTransform(@Nullable LocationTransform transform) {
|
|
1525
|
+
sLocationTransform = transform;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
public static @Nullable LocationTransform getLocationTransform() {
|
|
1529
|
+
return sLocationTransform;
|
|
1530
|
+
}
|
|
1531
|
+
}
|