@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.
Files changed (207) hide show
  1. package/JosuelmmCapacitorBackgroundGeolocation.podspec +34 -0
  2. package/LICENSE +17 -0
  3. package/NOTICE.md +32 -0
  4. package/Package.swift +45 -0
  5. package/README.md +402 -0
  6. package/android/build.gradle +79 -0
  7. package/android/proguard-rules.pro +1 -0
  8. package/android/src/main/AndroidManifest.xml +83 -0
  9. package/android/src/main/java/com/evgenii/jsevaluator/HandlerWrapper.java +18 -0
  10. package/android/src/main/java/com/evgenii/jsevaluator/JavaScriptInterface.java +22 -0
  11. package/android/src/main/java/com/evgenii/jsevaluator/JsEvaluator.java +133 -0
  12. package/android/src/main/java/com/evgenii/jsevaluator/JsFunctionCallFormatter.java +37 -0
  13. package/android/src/main/java/com/evgenii/jsevaluator/WebViewWrapper.java +71 -0
  14. package/android/src/main/java/com/evgenii/jsevaluator/interfaces/CallJavaResultInterface.java +8 -0
  15. package/android/src/main/java/com/evgenii/jsevaluator/interfaces/HandlerWrapperInterface.java +5 -0
  16. package/android/src/main/java/com/evgenii/jsevaluator/interfaces/JsCallback.java +10 -0
  17. package/android/src/main/java/com/evgenii/jsevaluator/interfaces/JsEvaluatorInterface.java +18 -0
  18. package/android/src/main/java/com/evgenii/jsevaluator/interfaces/WebViewWrapperInterface.java +14 -0
  19. package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/BackgroundGeolocationPlugin.java +898 -0
  20. package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/ConfigMapper.java +303 -0
  21. package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/HeadlessTaskRegistry.java +34 -0
  22. package/android/src/main/java/com/josuelmm/capacitor/backgroundgeolocation/JsEvaluatorTaskRunner.java +63 -0
  23. package/android/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +699 -0
  24. package/android/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +103 -0
  25. package/android/src/main/java/com/marianhello/bgloc/Config.java +1155 -0
  26. package/android/src/main/java/com/marianhello/bgloc/ConnectivityListener.java +5 -0
  27. package/android/src/main/java/com/marianhello/bgloc/HttpPostService.java +362 -0
  28. package/android/src/main/java/com/marianhello/bgloc/LocationManager.java +138 -0
  29. package/android/src/main/java/com/marianhello/bgloc/PluginDelegate.java +45 -0
  30. package/android/src/main/java/com/marianhello/bgloc/PluginException.java +38 -0
  31. package/android/src/main/java/com/marianhello/bgloc/PostLocationTask.java +238 -0
  32. package/android/src/main/java/com/marianhello/bgloc/ResourceResolver.java +55 -0
  33. package/android/src/main/java/com/marianhello/bgloc/data/AbstractLocationTemplate.java +69 -0
  34. package/android/src/main/java/com/marianhello/bgloc/data/ArrayListLocationTemplate.java +88 -0
  35. package/android/src/main/java/com/marianhello/bgloc/data/BackgroundActivity.java +108 -0
  36. package/android/src/main/java/com/marianhello/bgloc/data/BackgroundLocation.java +1088 -0
  37. package/android/src/main/java/com/marianhello/bgloc/data/ConfigJsonMapper.java +211 -0
  38. package/android/src/main/java/com/marianhello/bgloc/data/ConfigurationDAO.java +13 -0
  39. package/android/src/main/java/com/marianhello/bgloc/data/DAOFactory.java +17 -0
  40. package/android/src/main/java/com/marianhello/bgloc/data/HashMapLocationTemplate.java +82 -0
  41. package/android/src/main/java/com/marianhello/bgloc/data/LocationDAO.java +27 -0
  42. package/android/src/main/java/com/marianhello/bgloc/data/LocationTemplate.java +12 -0
  43. package/android/src/main/java/com/marianhello/bgloc/data/LocationTemplateFactory.java +71 -0
  44. package/android/src/main/java/com/marianhello/bgloc/data/LocationTransform.java +19 -0
  45. package/android/src/main/java/com/marianhello/bgloc/data/SessionLocationDAO.java +18 -0
  46. package/android/src/main/java/com/marianhello/bgloc/data/provider/ContentProviderLocationDAO.java +406 -0
  47. package/android/src/main/java/com/marianhello/bgloc/data/provider/LocationContentProvider.java +321 -0
  48. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationContract.java +94 -0
  49. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationDAO.java +227 -0
  50. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationContract.java +122 -0
  51. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationDAO.java +550 -0
  52. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +189 -0
  53. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionContract.java +74 -0
  54. package/android/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionLocationDAO.java +169 -0
  55. package/android/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
  56. package/android/src/main/java/com/marianhello/bgloc/headless/AbstractTaskRunner.java +15 -0
  57. package/android/src/main/java/com/marianhello/bgloc/headless/ActivityTask.java +48 -0
  58. package/android/src/main/java/com/marianhello/bgloc/headless/JsCallback.java +10 -0
  59. package/android/src/main/java/com/marianhello/bgloc/headless/LocationTask.java +60 -0
  60. package/android/src/main/java/com/marianhello/bgloc/headless/StationaryTask.java +25 -0
  61. package/android/src/main/java/com/marianhello/bgloc/headless/Task.java +8 -0
  62. package/android/src/main/java/com/marianhello/bgloc/headless/TaskRunner.java +5 -0
  63. package/android/src/main/java/com/marianhello/bgloc/headless/TaskRunnerFactory.java +8 -0
  64. package/android/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
  65. package/android/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
  66. package/android/src/main/java/com/marianhello/bgloc/provider/AbstractLocationProvider.java +218 -0
  67. package/android/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +385 -0
  68. package/android/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +685 -0
  69. package/android/src/main/java/com/marianhello/bgloc/provider/LocationProvider.java +32 -0
  70. package/android/src/main/java/com/marianhello/bgloc/provider/LocationProviderFactory.java +47 -0
  71. package/android/src/main/java/com/marianhello/bgloc/provider/ProviderDelegate.java +12 -0
  72. package/android/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +175 -0
  73. package/android/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
  74. package/android/src/main/java/com/marianhello/bgloc/service/LocationService.java +16 -0
  75. package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +1531 -0
  76. package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceInfo.java +6 -0
  77. package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceInfoImpl.java +41 -0
  78. package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceIntentBuilder.java +203 -0
  79. package/android/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +156 -0
  80. package/android/src/main/java/com/marianhello/bgloc/sync/AccountHelper.java +39 -0
  81. package/android/src/main/java/com/marianhello/bgloc/sync/Authenticator.java +68 -0
  82. package/android/src/main/java/com/marianhello/bgloc/sync/AuthenticatorService.java +28 -0
  83. package/android/src/main/java/com/marianhello/bgloc/sync/BatchManager.java +311 -0
  84. package/android/src/main/java/com/marianhello/bgloc/sync/NotificationHelper.java +148 -0
  85. package/android/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +301 -0
  86. package/android/src/main/java/com/marianhello/bgloc/sync/SyncService.java +68 -0
  87. package/android/src/main/java/com/marianhello/logging/DBLogReader.java +208 -0
  88. package/android/src/main/java/com/marianhello/logging/LogEntry.java +99 -0
  89. package/android/src/main/java/com/marianhello/logging/LoggerManager.java +70 -0
  90. package/android/src/main/java/com/marianhello/logging/UncaughtExceptionLogger.java +36 -0
  91. package/android/src/main/java/com/marianhello/utils/CloneHelper.java +22 -0
  92. package/android/src/main/java/com/marianhello/utils/Convert.java +56 -0
  93. package/android/src/main/java/com/marianhello/utils/TextUtils.java +72 -0
  94. package/android/src/main/java/com/marianhello/utils/ToneGenerator.java +68 -0
  95. package/android/src/main/java/org/apache/commons/io/Charsets.java +153 -0
  96. package/android/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java +344 -0
  97. package/android/src/main/java/org/chromium/content/browser/ThreadUtils.java +134 -0
  98. package/android/src/main/java/ru/andremoniy/sqlbuilder/SqlExpression.java +398 -0
  99. package/android/src/main/java/ru/andremoniy/sqlbuilder/SqlSelectStatement.java +671 -0
  100. package/android/src/main/java/ru/andremoniy/sqlbuilder/SqlStatement.java +29 -0
  101. package/android/src/main/java/ru/andremoniy/utils/TextUtils.java +61 -0
  102. package/android/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  103. package/android/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  104. package/android/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  105. package/android/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  106. package/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  107. package/android/src/main/res/values/strings.xml +15 -0
  108. package/android/src/main/res/xml/authenticator.xml +7 -0
  109. package/android/src/main/res/xml/syncadapter.xml +9 -0
  110. package/dist/esm/definitions.d.ts +1052 -0
  111. package/dist/esm/definitions.js +142 -0
  112. package/dist/esm/definitions.js.map +1 -0
  113. package/dist/esm/index.d.ts +8 -0
  114. package/dist/esm/index.js +23 -0
  115. package/dist/esm/index.js.map +1 -0
  116. package/dist/esm/web.d.ts +92 -0
  117. package/dist/esm/web.js +242 -0
  118. package/dist/esm/web.js.map +1 -0
  119. package/dist/plugin.cjs.js +415 -0
  120. package/dist/plugin.cjs.js.map +1 -0
  121. package/dist/plugin.js +418 -0
  122. package/dist/plugin.js.map +1 -0
  123. package/ios/Sources/BackgroundGeolocationPlugin/BackgroundGeolocationPlugin-Bridging-Header.h +18 -0
  124. package/ios/Sources/BackgroundGeolocationPlugin/BackgroundGeolocationPlugin.m +52 -0
  125. package/ios/Sources/BackgroundGeolocationPlugin/BackgroundGeolocationPlugin.swift +750 -0
  126. package/ios/Tests/BackgroundGeolocationPluginTests/BackgroundGeolocationPluginTests.swift +12 -0
  127. package/ios/common/BackgroundGeolocation/CocoaLumberjack.h +1945 -0
  128. package/ios/common/BackgroundGeolocation/CocoaLumberjack.m +5255 -0
  129. package/ios/common/BackgroundGeolocation/FMDB.h +2357 -0
  130. package/ios/common/BackgroundGeolocation/FMDB.m +2672 -0
  131. package/ios/common/BackgroundGeolocation/FMDBLogger.h +42 -0
  132. package/ios/common/BackgroundGeolocation/FMDBLogger.m +264 -0
  133. package/ios/common/BackgroundGeolocation/INTULocationManager/INTUHeadingRequest.h +41 -0
  134. package/ios/common/BackgroundGeolocation/INTULocationManager/INTUHeadingRequest.m +68 -0
  135. package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationManager+Internal.h +33 -0
  136. package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationManager.h +178 -0
  137. package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationManager.m +1025 -0
  138. package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationRequest.h +103 -0
  139. package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationRequest.m +238 -0
  140. package/ios/common/BackgroundGeolocation/INTULocationManager/INTULocationRequestDefines.h +163 -0
  141. package/ios/common/BackgroundGeolocation/INTULocationManager/INTURequestIDGenerator.h +39 -0
  142. package/ios/common/BackgroundGeolocation/INTULocationManager/INTURequestIDGenerator.m +37 -0
  143. package/ios/common/BackgroundGeolocation/MAURAbstractLocationProvider.h +51 -0
  144. package/ios/common/BackgroundGeolocation/MAURAbstractLocationProvider.m +53 -0
  145. package/ios/common/BackgroundGeolocation/MAURActivity.h +23 -0
  146. package/ios/common/BackgroundGeolocation/MAURActivity.m +52 -0
  147. package/ios/common/BackgroundGeolocation/MAURActivityLocationProvider.h +18 -0
  148. package/ios/common/BackgroundGeolocation/MAURActivityLocationProvider.m +340 -0
  149. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +88 -0
  150. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +1193 -0
  151. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +46 -0
  152. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +283 -0
  153. package/ios/common/BackgroundGeolocation/MAURBackgroundTaskManager.h +25 -0
  154. package/ios/common/BackgroundGeolocation/MAURBackgroundTaskManager.m +105 -0
  155. package/ios/common/BackgroundGeolocation/MAURConfig.h +99 -0
  156. package/ios/common/BackgroundGeolocation/MAURConfig.m +636 -0
  157. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.h +53 -0
  158. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.m +54 -0
  159. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.h +20 -0
  160. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +550 -0
  161. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.h +17 -0
  162. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +124 -0
  163. package/ios/common/BackgroundGeolocation/MAURLocation.h +73 -0
  164. package/ios/common/BackgroundGeolocation/MAURLocation.m +392 -0
  165. package/ios/common/BackgroundGeolocation/MAURLocationContract.h +38 -0
  166. package/ios/common/BackgroundGeolocation/MAURLocationContract.m +39 -0
  167. package/ios/common/BackgroundGeolocation/MAURLocationManager.h +53 -0
  168. package/ios/common/BackgroundGeolocation/MAURLocationManager.m +305 -0
  169. package/ios/common/BackgroundGeolocation/MAURLogReader.h +26 -0
  170. package/ios/common/BackgroundGeolocation/MAURLogReader.m +122 -0
  171. package/ios/common/BackgroundGeolocation/MAURLogging.h +19 -0
  172. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +53 -0
  173. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +367 -0
  174. package/ios/common/BackgroundGeolocation/MAURProviderDelegate.h +52 -0
  175. package/ios/common/BackgroundGeolocation/MAURRawLocationProvider.h +18 -0
  176. package/ios/common/BackgroundGeolocation/MAURRawLocationProvider.m +138 -0
  177. package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.h +26 -0
  178. package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.m +335 -0
  179. package/ios/common/BackgroundGeolocation/MAURSQLiteHelper.h +57 -0
  180. package/ios/common/BackgroundGeolocation/MAURSQLiteHelper.m +93 -0
  181. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.h +52 -0
  182. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.m +520 -0
  183. package/ios/common/BackgroundGeolocation/MAURSQLiteOpenHelper.h +32 -0
  184. package/ios/common/BackgroundGeolocation/MAURSQLiteOpenHelper.m +276 -0
  185. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
  186. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
  187. package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.h +29 -0
  188. package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.m +31 -0
  189. package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.h +25 -0
  190. package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.m +153 -0
  191. package/ios/common/BackgroundGeolocation/MAURUncaughtExceptionLogger.h +20 -0
  192. package/ios/common/BackgroundGeolocation/MAURUncaughtExceptionLogger.m +62 -0
  193. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
  194. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
  195. package/ios/common/BackgroundGeolocation/Reachability.h +102 -0
  196. package/ios/common/BackgroundGeolocation/Reachability.m +475 -0
  197. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/README.md +170 -0
  198. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/ext/NSString+ZIMString.h +55 -0
  199. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/ext/NSString+ZIMString.m +47 -0
  200. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlDataManipulationCommand.h +27 -0
  201. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlExpression.h +250 -0
  202. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlExpression.m +259 -0
  203. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlSelectStatement.h +360 -0
  204. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlSelectStatement.m +427 -0
  205. package/ios/common/BackgroundGeolocation/SQLQueryBuilder/sql/ZIMSqlStatement.h +37 -0
  206. package/ios/common/BackgroundGeolocation/module.modulemap +16 -0
  207. package/package.json +82 -0
@@ -0,0 +1,301 @@
1
+ package com.marianhello.bgloc.sync;
2
+
3
+ import android.accounts.Account;
4
+ import android.app.NotificationManager;
5
+ import android.content.AbstractThreadedSyncAdapter;
6
+ import android.content.ContentProviderClient;
7
+ import android.content.ContentResolver;
8
+ import android.content.Context;
9
+ import android.content.Intent;
10
+ import android.content.SyncResult;
11
+ import android.os.Bundle;
12
+ import android.os.Handler;
13
+ import android.os.Looper;
14
+ import androidx.core.app.NotificationCompat;
15
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
16
+
17
+ import com.marianhello.bgloc.Config;
18
+ import com.marianhello.bgloc.HttpPostService;
19
+ import com.marianhello.bgloc.data.ConfigurationDAO;
20
+ import com.marianhello.bgloc.data.DAOFactory;
21
+ import com.marianhello.bgloc.service.LocationServiceImpl;
22
+ import com.marianhello.logging.LoggerManager;
23
+
24
+ import org.json.JSONException;
25
+
26
+ import java.io.File;
27
+ import java.io.IOException;
28
+ import java.util.HashMap;
29
+
30
+ /**
31
+ * Handle the transfer of data between a server and an
32
+ * app, using the Android sync adapter framework.
33
+ */
34
+ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPostService.UploadingProgressListener {
35
+
36
+ private static final int NOTIFICATION_ID = 666;
37
+
38
+ ContentResolver contentResolver;
39
+ private ConfigurationDAO configDAO;
40
+ private NotificationManager notificationManager;
41
+ private BatchManager batchManager;
42
+ private boolean notificationsEnabled = true;
43
+ private volatile Config currentSyncConfig;
44
+
45
+ private org.slf4j.Logger logger;
46
+
47
+ /**
48
+ * Set up the sync adapter
49
+ */
50
+ public SyncAdapter(Context context, boolean autoInitialize) {
51
+ this(context, autoInitialize, false);
52
+ }
53
+
54
+
55
+ /**
56
+ * Set up the sync adapter. This form of the
57
+ * constructor maintains compatibility with Android 3.0
58
+ * and later platform versions
59
+ */
60
+ public SyncAdapter(
61
+ Context context,
62
+ boolean autoInitialize,
63
+ boolean allowParallelSyncs) {
64
+
65
+ super(context, autoInitialize);
66
+ logger = LoggerManager.getLogger(SyncAdapter.class);
67
+
68
+ /*
69
+ * If your app uses a content resolver, get an instance of it
70
+ * from the incoming Context
71
+ */
72
+ contentResolver = context.getContentResolver();
73
+ configDAO = DAOFactory.createConfigurationDAO(context);
74
+ batchManager = new BatchManager(this.getContext());
75
+ notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
76
+
77
+ NotificationHelper.registerSyncChannel(context);
78
+ }
79
+
80
+ /*
81
+ * Specify the code you want to run in the sync adapter. The entire
82
+ * sync adapter runs in a background thread, so you don't have to set
83
+ * up your own background processing.
84
+ */
85
+ @Override
86
+ public void onPerformSync(
87
+ Account account,
88
+ Bundle extras,
89
+ String authority,
90
+ ContentProviderClient provider,
91
+ SyncResult syncResult) {
92
+
93
+ Config config = null;
94
+ try {
95
+ config = configDAO.retrieveConfiguration();
96
+ } catch (JSONException e) {
97
+ logger.error("Error retrieving config: {}", e.getMessage());
98
+ syncResult.stats.numParseExceptions++;
99
+ return;
100
+ }
101
+
102
+ if (config == null || !config.hasValidSyncUrl() || !Boolean.TRUE.equals(config.getSyncEnabled())) {
103
+ if (config == null) {
104
+ logger.warn("Sync skipped: no config");
105
+ } else if (!Boolean.TRUE.equals(config.getSyncEnabled())) {
106
+ logger.info("Sync skipped: sync disabled in config");
107
+ }
108
+ return;
109
+ }
110
+
111
+ //noinspection ConstantConditions
112
+ notificationsEnabled = !config.hasNotificationsEnabled() || config.getNotificationsEnabled();
113
+ currentSyncConfig = config;
114
+
115
+ Long batchStartMillis = System.currentTimeMillis();
116
+ boolean isForced = (extras != null) && extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
117
+ Integer configThreshold = config.getSyncThreshold();
118
+ int syncThreshold = isForced ? 0 : (configThreshold != null ? configThreshold : 100);
119
+ logger.debug("Sync request isForced: {}, batchId: {}, syncThreshold: {}, config: {}", isForced, batchStartMillis, syncThreshold, config.toString());
120
+
121
+ File file = null;
122
+ try {
123
+ file = batchManager.createBatch(batchStartMillis, syncThreshold, config.getTemplate());
124
+ } catch (IOException e) {
125
+ logger.error("Failed to create batch: {}", e.getMessage());
126
+ syncResult.stats.numIoExceptions++;
127
+ return;
128
+ }
129
+
130
+ if (file == null) {
131
+ logger.info("Nothing to sync");
132
+ return;
133
+ }
134
+
135
+ logger.info("Syncing startAt: {}", batchStartMillis);
136
+ String url = config.getSyncUrl();
137
+ HashMap<String, String> httpHeaders = new HashMap<String, String>();
138
+ if (config.getHttpHeaders() != null) {
139
+ httpHeaders.putAll(config.getHttpHeaders());
140
+ }
141
+ httpHeaders.put("x-batch-id", String.valueOf(batchStartMillis));
142
+
143
+ // For URL templating in sync mode we can only resolve static queryParams keys; per-location
144
+ // placeholders (like {lat}) cannot apply to a multi-location batch. If the user wants per-location
145
+ // URL substitution they should use httpMode="single" + url= ... (real-time) or syncMode="single".
146
+ String resolvedUrl = com.marianhello.bgloc.http.UrlTemplateResolver.resolve(url, null, config.getQueryParams());
147
+ String syncMethod = config.getSyncHttpMethod();
148
+ if (uploadLocations(file, resolvedUrl, httpHeaders, syncMethod)) {
149
+ logger.info("Batch sync successful");
150
+ batchManager.setBatchCompleted(batchStartMillis);
151
+ if (file.delete()) {
152
+ logger.info("Batch file has been deleted: {}", file.getAbsolutePath());
153
+ } else {
154
+ logger.warn("Batch file has not been deleted: {}", file.getAbsolutePath());
155
+ }
156
+ } else {
157
+ logger.warn("Batch sync failed due server error");
158
+ syncResult.stats.numIoExceptions++;
159
+ }
160
+ }
161
+
162
+ private boolean uploadLocations(File file, String url, HashMap httpHeaders, String method) {
163
+ NotificationCompat.Builder builder = null;
164
+
165
+ if (notificationsEnabled) {
166
+ builder = new NotificationCompat.Builder(getContext(), NotificationHelper.SYNC_CHANNEL_ID);
167
+ builder.setOngoing(true);
168
+ builder.setContentTitle(currentSyncConfig.getNotificationSyncTitle());
169
+ builder.setContentText(currentSyncConfig.getNotificationSyncText());
170
+ builder.setSmallIcon(android.R.drawable.ic_dialog_info);
171
+ notificationManager.notify(NOTIFICATION_ID, builder.build());
172
+ }
173
+
174
+ // v3.5 Phase 4: emit syncStart event.
175
+ Bundle syncStart = new Bundle();
176
+ syncStart.putInt("action", LocationServiceImpl.MSG_ON_SYNC_START);
177
+ broadcastMessage(syncStart);
178
+
179
+ // Count locations being uploaded (best-effort, API-21+ safe).
180
+ int locationsAttempted = 0;
181
+ java.io.FileInputStream fis = null;
182
+ try {
183
+ fis = new java.io.FileInputStream(file);
184
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
185
+ byte[] buf = new byte[4096];
186
+ int n;
187
+ while ((n = fis.read(buf)) > 0) baos.write(buf, 0, n);
188
+ org.json.JSONArray arr = new org.json.JSONArray(new String(baos.toByteArray(), "UTF-8"));
189
+ locationsAttempted = arr.length();
190
+ } catch (Throwable ignored) { /* best-effort; emit 0 if we cannot read */
191
+ } finally {
192
+ if (fis != null) try { fis.close(); } catch (Exception ignored) {}
193
+ }
194
+
195
+ try {
196
+ int responseCode = HttpPostService.postJSONFile(url, file, httpHeaders, this, method);
197
+
198
+ // All 2xx statuses are okay
199
+ boolean isStatusOkay = responseCode >= 200 && responseCode < 300;
200
+
201
+ if (responseCode == 285) {
202
+ // Okay, but we don't need to continue sending these
203
+
204
+ logger.debug("Location was sent to the server, and received an \"HTTP 285 Updates Not Required\"");
205
+
206
+ Bundle bundle = new Bundle();
207
+ bundle.putInt("action", LocationServiceImpl.MSG_ON_ABORT_REQUESTED);
208
+ broadcastMessage(bundle);
209
+ }
210
+
211
+ if (responseCode == 401) {
212
+ Bundle bundle = new Bundle();
213
+ bundle.putInt("action", LocationServiceImpl.MSG_ON_HTTP_AUTHORIZATION);
214
+ broadcastMessage(bundle);
215
+ }
216
+
217
+ if (builder != null) {
218
+ if (isStatusOkay) {
219
+ builder.setContentText(currentSyncConfig.getNotificationSyncCompletedText());
220
+ } else {
221
+ builder.setContentText(currentSyncConfig.getNotificationSyncFailedText() + " (HTTP " + responseCode + ")");
222
+ }
223
+ }
224
+
225
+ if (!isStatusOkay) {
226
+ logger.warn("Batch sync failed: server returned HTTP {} (check server logs or sync URL)", responseCode);
227
+ Bundle errBundle = new Bundle();
228
+ errBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_ERROR);
229
+ errBundle.putInt("httpStatus", responseCode);
230
+ errBundle.putString("message", "HTTP " + responseCode);
231
+ broadcastMessage(errBundle);
232
+ } else {
233
+ Bundle okBundle = new Bundle();
234
+ okBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_SUCCESS);
235
+ okBundle.putInt("sent", locationsAttempted);
236
+ broadcastMessage(okBundle);
237
+ }
238
+
239
+ return isStatusOkay;
240
+ } catch (IOException e) {
241
+ String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
242
+ logger.warn("Error uploading locations (network/IO): {}", errMsg);
243
+
244
+ if (builder != null) {
245
+ builder.setContentText(currentSyncConfig.getNotificationSyncFailedText() + ": " + errMsg);
246
+ }
247
+ Bundle errBundle = new Bundle();
248
+ errBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_ERROR);
249
+ errBundle.putInt("httpStatus", 0);
250
+ errBundle.putString("message", errMsg);
251
+ broadcastMessage(errBundle);
252
+ } finally {
253
+ logger.info("Syncing endAt: {}", System.currentTimeMillis());
254
+
255
+ if (builder != null) {
256
+ builder.setOngoing(false);
257
+ builder.setProgress(0, 0, false);
258
+ builder.setAutoCancel(true);
259
+ notificationManager.notify(NOTIFICATION_ID, builder.build());
260
+
261
+ Handler h = new Handler(Looper.getMainLooper());
262
+ long delayInMilliseconds = 5000;
263
+ h.postDelayed(new Runnable() {
264
+ public void run() {
265
+ logger.info("Notification cancelledAt: {}", System.currentTimeMillis());
266
+ notificationManager.cancel(NOTIFICATION_ID);
267
+ }
268
+ }, delayInMilliseconds);
269
+ }
270
+ }
271
+
272
+ return false;
273
+ }
274
+
275
+ public void onProgress(int progress) {
276
+ logger.debug("Syncing progress: {} updatedAt: {}", progress, System.currentTimeMillis());
277
+
278
+ Config c = currentSyncConfig;
279
+ if (notificationsEnabled && c != null) {
280
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext(), NotificationHelper.SYNC_CHANNEL_ID);
281
+ builder.setOngoing(true);
282
+ builder.setContentTitle(c.getNotificationSyncTitle());
283
+ builder.setContentText(c.getNotificationSyncText());
284
+ builder.setSmallIcon(android.R.drawable.ic_dialog_info);
285
+ builder.setProgress(100, progress, false);
286
+ notificationManager.notify(NOTIFICATION_ID, builder.build());
287
+ }
288
+
289
+ // v3.5 Phase 4: forward progress percentage to JS via syncProgress event.
290
+ Bundle progBundle = new Bundle();
291
+ progBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_PROGRESS);
292
+ progBundle.putInt("progress", progress);
293
+ broadcastMessage(progBundle);
294
+ }
295
+
296
+ private void broadcastMessage(Bundle bundle) {
297
+ Intent intent = new Intent(LocationServiceImpl.ACTION_BROADCAST);
298
+ intent.putExtras(bundle);
299
+ LocalBroadcastManager.getInstance(getContext().getApplicationContext()).sendBroadcast(intent);
300
+ }
301
+ }
@@ -0,0 +1,68 @@
1
+ package com.marianhello.bgloc.sync;
2
+
3
+ import android.accounts.Account;
4
+ import android.app.Service;
5
+ import android.content.ContentResolver;
6
+ import android.content.Intent;
7
+ import android.os.Bundle;
8
+ import android.os.IBinder;
9
+
10
+ /**
11
+ * Define a Service that returns an IBinder for the
12
+ * sync adapter class, allowing the sync adapter framework to call
13
+ * onPerformSync().
14
+ */
15
+ public class SyncService extends Service {
16
+ // Storage for an instance of the sync adapter
17
+ private static SyncAdapter sSyncAdapter = null;
18
+ // Object to use as a thread-safe lock
19
+ private static final Object sSyncAdapterLock = new Object();
20
+
21
+ /**
22
+ * Instantiate the sync adapter object.
23
+ */
24
+ @Override
25
+ public void onCreate() {
26
+ /*
27
+ * Create the sync adapter as a singleton.
28
+ * Set the sync adapter as syncable
29
+ * Disallow parallel syncs
30
+ */
31
+ synchronized (sSyncAdapterLock) {
32
+ if (sSyncAdapter == null) {
33
+ sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Return an object that allows the system to invoke
40
+ * the sync adapter.
41
+ *
42
+ */
43
+ @Override
44
+ public IBinder onBind(Intent intent) {
45
+ /*
46
+ * Get the object that allows external processes
47
+ * to call onPerformSync(). The object is created
48
+ * in the base class code when the SyncAdapter
49
+ * constructors call super()
50
+ */
51
+ return sSyncAdapter.getSyncAdapterBinder();
52
+ }
53
+
54
+ public static void sync(Account account, String authority, boolean manual) {
55
+ // Pass the settings flags by inserting them in a bundle
56
+ Bundle settingsBundle = new Bundle();
57
+ settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, manual);
58
+ settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, manual);
59
+ settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false);
60
+ settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
61
+
62
+ /*
63
+ * Request the sync for the default account, authority, and
64
+ * manual sync settings
65
+ */
66
+ ContentResolver.requestSync(account, authority, settingsBundle);
67
+ }
68
+ }
@@ -0,0 +1,208 @@
1
+ package com.marianhello.logging;
2
+
3
+ import android.database.Cursor;
4
+ import android.database.sqlite.SQLiteDatabase;
5
+ import android.database.sqlite.SQLiteException;
6
+
7
+ import ru.andremoniy.sqlbuilder.SqlExpression;
8
+ import ru.andremoniy.sqlbuilder.SqlSelectStatement;
9
+
10
+ import org.slf4j.event.Level;
11
+
12
+ import java.io.File;
13
+ import java.sql.SQLException;
14
+ import java.util.ArrayList;
15
+ import java.util.Collection;
16
+
17
+ import ch.qos.logback.classic.LoggerContext;
18
+ import ch.qos.logback.classic.db.names.ColumnName;
19
+ import ch.qos.logback.classic.db.names.DBNameResolver;
20
+ import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
21
+ import ch.qos.logback.classic.db.names.TableName;
22
+ import ch.qos.logback.core.CoreConstants;
23
+ // NOTE: logback-android 2.x / 3.x dropped `ch.qos.logback.core.android.CommonPathUtil`.
24
+ // The dropped util only computed `/data/data/<pkg>/databases`, so we inline that
25
+ // path below. Adjusted from the upstream `mauron85/cordova-plugin-background-geolocation`
26
+ // source for Capacitor 8+ compatibility. Apache-2.0.
27
+
28
+ public class DBLogReader {
29
+
30
+ public static final String DB_FILENAME = "logback.db";
31
+
32
+ private DefaultDBNameResolver mDbNameResolver;
33
+ private SQLiteDatabase mDatabase;
34
+
35
+ public static class QueryBuilder {
36
+ DBNameResolver mDbNameResolver;
37
+
38
+ public QueryBuilder() {
39
+ mDbNameResolver = new DefaultDBNameResolver();
40
+ }
41
+
42
+ public QueryBuilder(DBNameResolver dbNameResolver) {
43
+ mDbNameResolver = dbNameResolver;
44
+ }
45
+
46
+ /**
47
+ * Generate array of levels that are same or above provided level
48
+ *
49
+ * @param level
50
+ * @return array of levels that are same or above level
51
+ */
52
+ private Object[] aboveLevel(Level level) {
53
+ ArrayList<String> levels = new ArrayList();
54
+ for (Level l : Level.values()) {
55
+ if (level.compareTo(l) >= 0) {
56
+ levels.add(l.toString());
57
+ }
58
+ }
59
+ return levels.toArray();
60
+ }
61
+
62
+ public String buildStackTraceQuery(int eventId) {
63
+ SqlSelectStatement builder = new SqlSelectStatement();
64
+ builder.column(mDbNameResolver.getColumnName(ColumnName.TRACE_LINE));
65
+ builder.from(mDbNameResolver.getTableName(TableName.LOGGING_EVENT_EXCEPTION));
66
+ builder.where(mDbNameResolver.getColumnName(ColumnName.I), SqlExpression.SqlOperatorEqualTo, Integer.valueOf(eventId));
67
+ builder.orderBy(mDbNameResolver.getColumnName(ColumnName.I));
68
+
69
+ return builder.statement();
70
+ }
71
+
72
+ public String buildQuery(int limit, int fromLogEntryId, Level minLevel) {
73
+ SqlSelectStatement builder = new SqlSelectStatement();
74
+ builder.columns(new String[]{
75
+ mDbNameResolver.getColumnName(ColumnName.EVENT_ID),
76
+ mDbNameResolver.getColumnName(ColumnName.TIMESTMP),
77
+ mDbNameResolver.getColumnName(ColumnName.FORMATTED_MESSAGE),
78
+ mDbNameResolver.getColumnName(ColumnName.LOGGER_NAME),
79
+ mDbNameResolver.getColumnName(ColumnName.LEVEL_STRING),
80
+ });
81
+ builder.from(mDbNameResolver.getTableName(TableName.LOGGING_EVENT));
82
+ builder.where(mDbNameResolver.getColumnName(ColumnName.LEVEL_STRING), SqlExpression.SqlOperatorIn, aboveLevel(minLevel));
83
+ if (fromLogEntryId > 0) {
84
+ if (limit >= 0) {
85
+ builder.where(mDbNameResolver.getColumnName(ColumnName.EVENT_ID), SqlExpression.SqlOperatorLessThan, fromLogEntryId);
86
+ } else {
87
+ builder.where(mDbNameResolver.getColumnName(ColumnName.EVENT_ID), SqlExpression.SqlOperatorGreaterThan, fromLogEntryId);
88
+ }
89
+ }
90
+ if (limit < 0) {
91
+ builder.orderBy(mDbNameResolver.getColumnName(ColumnName.TIMESTMP));
92
+ builder.orderBy(mDbNameResolver.getColumnName(ColumnName.EVENT_ID));
93
+ } else {
94
+ builder.orderBy(mDbNameResolver.getColumnName(ColumnName.TIMESTMP), true);
95
+ builder.orderBy(mDbNameResolver.getColumnName(ColumnName.EVENT_ID), true);
96
+ }
97
+ builder.limit(limit);
98
+
99
+ return builder.statement();
100
+ }
101
+ }
102
+
103
+ public Collection<LogEntry> getEntries(int limit, int fromLogEntryId, Level minLevel) {
104
+ try {
105
+ return getDbEntries(limit, fromLogEntryId, minLevel);
106
+ } catch (SQLException e) {
107
+ e.printStackTrace();
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ private SQLiteDatabase openDatabase() throws SQLException {
114
+ if (mDatabase != null && mDatabase.isOpen()) {
115
+ return mDatabase;
116
+ }
117
+
118
+ String packageName = null;
119
+ LoggerContext context = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();
120
+
121
+ if (context != null) {
122
+ packageName = context.getProperty(CoreConstants.PACKAGE_NAME_KEY);
123
+ }
124
+
125
+ if (packageName == null || packageName.length() == 0) {
126
+ throw new SQLException("Cannot open database without package name");
127
+ }
128
+
129
+ try {
130
+ // Android stores app-private databases at `/data/data/<package>/databases`.
131
+ File dbDir = new File("/data/data/" + packageName + "/databases");
132
+ File dbfile = new File(dbDir, DB_FILENAME);
133
+ mDatabase = SQLiteDatabase.openDatabase(dbfile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
134
+ } catch (SQLiteException e) {
135
+ throw new SQLException("Cannot open database", e);
136
+ }
137
+
138
+ return mDatabase;
139
+ }
140
+
141
+ private DefaultDBNameResolver getDbNameResolver() {
142
+ if (mDbNameResolver != null) {
143
+ return mDbNameResolver;
144
+ }
145
+
146
+ mDbNameResolver = new DefaultDBNameResolver();
147
+ return mDbNameResolver;
148
+ }
149
+
150
+ private Collection<String> getStackTrace(int logEntryId) throws SQLException {
151
+ Collection<String> stackTrace = new ArrayList();
152
+ SQLiteDatabase db = openDatabase();
153
+ Cursor cursor = null;
154
+
155
+ try {
156
+ DefaultDBNameResolver dbNameResolver = getDbNameResolver();
157
+ QueryBuilder qb = new QueryBuilder(dbNameResolver);
158
+ cursor = mDatabase.rawQuery(qb.buildStackTraceQuery(logEntryId), new String[] {});
159
+ while (cursor.moveToNext()) {
160
+ stackTrace.add(cursor.getString(cursor.getColumnIndex(dbNameResolver.getColumnName(ColumnName.TRACE_LINE))));
161
+ }
162
+ } catch (SQLiteException e) {
163
+ throw new SQLException("Cannot retrieve log entries", e);
164
+ } finally {
165
+ if (cursor != null) {
166
+ cursor.close();
167
+ }
168
+ }
169
+
170
+ return stackTrace;
171
+ }
172
+
173
+ private Collection<LogEntry> getDbEntries(int limit, int fromLogEntryId, Level minLevel) throws SQLException {
174
+ Collection<LogEntry> entries = new ArrayList<LogEntry>();
175
+ SQLiteDatabase db = openDatabase();
176
+ Cursor cursor = null;
177
+
178
+ try {
179
+ DefaultDBNameResolver dbNameResolver = getDbNameResolver();
180
+ QueryBuilder qb = new QueryBuilder(dbNameResolver);
181
+ cursor = db.rawQuery(qb.buildQuery(limit, fromLogEntryId, minLevel), new String[] {});
182
+ while (cursor.moveToNext()) {
183
+ LogEntry entry = new LogEntry();
184
+ entry.setContext(0);
185
+ entry.setId(cursor.getInt(cursor.getColumnIndex(mDbNameResolver.getColumnName(ColumnName.EVENT_ID))));
186
+ entry.setLevel(cursor.getString(cursor.getColumnIndex(dbNameResolver.getColumnName(ColumnName.LEVEL_STRING))));
187
+ entry.setMessage(cursor.getString(cursor.getColumnIndex(dbNameResolver.getColumnName(ColumnName.FORMATTED_MESSAGE))));
188
+ entry.setTimestamp(cursor.getLong(cursor.getColumnIndex(dbNameResolver.getColumnName(ColumnName.TIMESTMP))));
189
+ entry.setLoggerName(cursor.getString(cursor.getColumnIndex(dbNameResolver.getColumnName(ColumnName.LOGGER_NAME))));
190
+ if ("ERROR".equals(entry.getLevel())) {
191
+ entry.setStackTrace(getStackTrace(entry.getId()));
192
+ }
193
+ entries.add(entry);
194
+ }
195
+ } catch (SQLiteException e) {
196
+ throw new SQLException("Cannot retrieve log entries", e);
197
+ } finally {
198
+ if (cursor != null) {
199
+ cursor.close();
200
+ }
201
+ if (db != null) {
202
+ db.close();
203
+ }
204
+ }
205
+
206
+ return entries;
207
+ }
208
+ }
@@ -0,0 +1,99 @@
1
+ package com.marianhello.logging;
2
+
3
+ import org.json.JSONException;
4
+ import org.json.JSONObject;
5
+
6
+ import java.util.Collection;
7
+
8
+ public class LogEntry {
9
+ private Integer id;
10
+ private Integer context;
11
+ private String level;
12
+ private String message;
13
+ private Long timestamp;
14
+ private String loggerName;
15
+ private Collection<String> stackTrace;
16
+
17
+ public Integer getId() {
18
+ return id;
19
+ }
20
+
21
+ public void setId(Integer id) {
22
+ this.id = id;
23
+ }
24
+
25
+ public Integer getContext() {
26
+ return context;
27
+ }
28
+
29
+ public void setContext(Integer context) {
30
+ this.context = context;
31
+ }
32
+
33
+ public String getLevel() {
34
+ return level;
35
+ }
36
+
37
+ public void setLevel(String level) {
38
+ this.level = level;
39
+ }
40
+
41
+ public String getMessage() {
42
+ return message;
43
+ }
44
+
45
+ public void setMessage(String message) {
46
+ this.message = message;
47
+ }
48
+
49
+ public Long getTimestamp() {
50
+ return timestamp;
51
+ }
52
+
53
+ public void setTimestamp(Long timestamp) {
54
+ this.timestamp = timestamp;
55
+ }
56
+
57
+ public String getLoggerName() {
58
+ return loggerName;
59
+ }
60
+
61
+ public void setLoggerName(String loggerName) {
62
+ this.loggerName = loggerName;
63
+ }
64
+
65
+ public boolean hasStackTrace() {
66
+ return stackTrace != null;
67
+ }
68
+
69
+ public String getStackTrace() {
70
+ if (this.stackTrace == null) {
71
+ return null;
72
+ }
73
+
74
+ StringBuilder stackTraceBuilder = new StringBuilder();
75
+ for (String traceLine : this.stackTrace) {
76
+ stackTraceBuilder.append(traceLine).append("\n");
77
+ }
78
+ return stackTraceBuilder.toString();
79
+ }
80
+
81
+ public void setStackTrace(Collection<String> stackTrace) {
82
+ this.stackTrace = stackTrace;
83
+ }
84
+
85
+ public JSONObject toJSONObject() throws JSONException {
86
+ JSONObject json = new JSONObject();
87
+ json.put("id", this.id);
88
+ json.put("context", this.context);
89
+ json.put("level", this.level);
90
+ json.put("message", this.message);
91
+ json.put("timestamp", this.timestamp);
92
+ json.put("logger", this.loggerName);
93
+ if (hasStackTrace()) {
94
+ json.put("stackTrace", this.getStackTrace());
95
+ }
96
+
97
+ return json;
98
+ }
99
+ }