@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,1025 @@
1
+ //
2
+ // INTULocationManager.m
3
+ //
4
+ // Copyright (c) 2014-2017 Intuit Inc.
5
+ //
6
+ // Permission is hereby granted, free of charge, to any person obtaining
7
+ // a copy of this software and associated documentation files (the
8
+ // "Software"), to deal in the Software without restriction, including
9
+ // without limitation the rights to use, copy, modify, merge, publish,
10
+ // distribute, sublicense, and/or sell copies of the Software, and to
11
+ // permit persons to whom the Software is furnished to do so, subject to
12
+ // the following conditions:
13
+ //
14
+ // The above copyright notice and this permission notice shall be
15
+ // included in all copies or substantial portions of the Software.
16
+ //
17
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ //
25
+
26
+ #import "INTULocationManager.h"
27
+ #import "INTULocationManager+Internal.h"
28
+ #import "INTULocationRequest.h"
29
+ #import "INTUHeadingRequest.h"
30
+
31
+
32
+ #ifndef INTU_ENABLE_LOGGING
33
+ # ifdef DEBUG
34
+ # define INTU_ENABLE_LOGGING 1
35
+ # else
36
+ # define INTU_ENABLE_LOGGING 0
37
+ # endif /* DEBUG */
38
+ #endif /* INTU_ENABLE_LOGGING */
39
+
40
+ #if INTU_ENABLE_LOGGING
41
+ # define INTULMLog(...) NSLog(@"INTULocationManager: %@", [NSString stringWithFormat:__VA_ARGS__]);
42
+ #else
43
+ # define INTULMLog(...)
44
+ #endif /* INTU_ENABLE_LOGGING */
45
+
46
+
47
+ @interface INTULocationManager () <CLLocationManagerDelegate, INTULocationRequestDelegate>
48
+
49
+ /** The instance of CLLocationManager encapsulated by this class. */
50
+ @property (nonatomic, strong) CLLocationManager *locationManager;
51
+ /** The most recent current location, or nil if the current location is unknown, invalid, or stale. */
52
+ @property (nonatomic, strong) CLLocation *currentLocation;
53
+ /** The most recent current heading, or nil if the current heading is unknown, invalid, or stale. */
54
+ @property (nonatomic, strong) CLHeading *currentHeading;
55
+ /** Whether or not the CLLocationManager is currently monitoring significant location changes. */
56
+ @property (nonatomic, assign) BOOL isMonitoringSignificantLocationChanges;
57
+ /** Whether or not the CLLocationManager is currently sending location updates. */
58
+ @property (nonatomic, assign) BOOL isUpdatingLocation;
59
+ /** Whether or not the CLLocationManager is currently sending heading updates. */
60
+ @property (nonatomic, assign) BOOL isUpdatingHeading;
61
+ /** Whether an error occurred during the last location update. */
62
+ @property (nonatomic, assign) BOOL updateFailed;
63
+
64
+ // An array of active location requests in the form:
65
+ // @[ INTULocationRequest *locationRequest1, INTULocationRequest *locationRequest2, ... ]
66
+ @property (nonatomic, strong) __INTU_GENERICS(NSArray, INTULocationRequest *) *locationRequests;
67
+
68
+ // An array of active heading requests in the form:
69
+ // @[ INTUHeadingRequest *headingRequest1, INTUHeadingRequest *headingRequest2, ... ]
70
+ @property (nonatomic, strong) __INTU_GENERICS(NSArray, INTUHeadingRequest *) *headingRequests;
71
+
72
+ @end
73
+
74
+
75
+ @implementation INTULocationManager
76
+
77
+ static id _sharedInstance;
78
+
79
+ /**
80
+ Returns the current state of location services for this app, based on the system settings and user authorization status.
81
+ */
82
+ + (INTULocationServicesState)locationServicesState
83
+ {
84
+ if ([CLLocationManager locationServicesEnabled] == NO) {
85
+ return INTULocationServicesStateDisabled;
86
+ }
87
+ else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) {
88
+ return INTULocationServicesStateNotDetermined;
89
+ }
90
+ else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
91
+ return INTULocationServicesStateDenied;
92
+ }
93
+ else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusRestricted) {
94
+ return INTULocationServicesStateRestricted;
95
+ }
96
+
97
+ return INTULocationServicesStateAvailable;
98
+ }
99
+
100
+ /**
101
+ Returns the current state of heading services for this device.
102
+ */
103
+ + (INTUHeadingServicesState)headingServicesState
104
+ {
105
+ if ([CLLocationManager headingAvailable]) {
106
+ return INTUHeadingServicesStateAvailable;
107
+ } else {
108
+ return INTUHeadingServicesStateUnavailable;
109
+ }
110
+ }
111
+
112
+ /**
113
+ Returns the singleton instance of this class.
114
+ */
115
+ + (instancetype)sharedInstance
116
+ {
117
+ static dispatch_once_t _onceToken;
118
+ dispatch_once(&_onceToken, ^{
119
+ _sharedInstance = [[self alloc] init];
120
+ });
121
+ return _sharedInstance;
122
+ }
123
+
124
+ - (instancetype)init
125
+ {
126
+ NSAssert(_sharedInstance == nil, @"Only one instance of INTULocationManager should be created. Use +[INTULocationManager sharedInstance] instead.");
127
+ self = [super init];
128
+ if (self) {
129
+ _locationManager = [[CLLocationManager alloc] init];
130
+ _locationManager.delegate = self;
131
+
132
+ #ifdef __IPHONE_8_4
133
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_8_4
134
+ /* iOS 9 requires setting allowsBackgroundLocationUpdates to YES in order to receive background location updates.
135
+ We only set it to YES if the location background mode is enabled for this app, as the documentation suggests it is a
136
+ fatal programmer error otherwise. */
137
+ NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
138
+ if ([backgroundModes containsObject:@"location"]) {
139
+ if (@available(iOS 9, *)) {
140
+ [_locationManager setAllowsBackgroundLocationUpdates:YES];
141
+ }
142
+ }
143
+ #endif /* __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_8_4 */
144
+ #endif /* __IPHONE_8_4 */
145
+
146
+ _locationRequests = @[];
147
+ }
148
+ return self;
149
+ }
150
+
151
+ #pragma mark Public location methods
152
+
153
+ /**
154
+ Asynchronously requests the current location of the device using location services.
155
+
156
+ @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location).
157
+ @param timeout The maximum amount of time (in seconds) to wait for a location with the desired accuracy before completing.
158
+ If this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled).
159
+ @param block The block to be executed when the request succeeds, fails, or times out. Three parameters are passed into the block:
160
+ - The current location (the most recent one acquired, regardless of accuracy level), or nil if no valid location was acquired
161
+ - The achieved accuracy for the current location (may be less than the desired accuracy if the request failed)
162
+ - The request status (if it succeeded, or if not, why it failed)
163
+
164
+ @return The location request ID, which can be used to force early completion or cancel the request while it is in progress.
165
+ */
166
+ - (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy
167
+ timeout:(NSTimeInterval)timeout
168
+ block:(INTULocationRequestBlock)block
169
+ {
170
+ return [self requestLocationWithDesiredAccuracy:desiredAccuracy
171
+ timeout:timeout
172
+ delayUntilAuthorized:NO
173
+ block:block];
174
+ }
175
+
176
+ /**
177
+ Asynchronously requests the current location of the device using location services, optionally waiting until the user grants the app permission
178
+ to access location services before starting the timeout countdown.
179
+
180
+ @param desiredAccuracy The accuracy level desired (refers to the accuracy and recency of the location).
181
+ @param timeout The maximum amount of time (in seconds) to wait for a location with the desired accuracy before completing. If
182
+ this value is 0.0, no timeout will be set (will wait indefinitely for success, unless request is force completed or canceled).
183
+ @param delayUntilAuthorized A flag specifying whether the timeout should only take effect after the user responds to the system prompt requesting
184
+ permission for this app to access location services. If YES, the timeout countdown will not begin until after the
185
+ app receives location services permissions. If NO, the timeout countdown begins immediately when calling this method.
186
+ @param block The block to be executed when the request succeeds, fails, or times out. Three parameters are passed into the block:
187
+ - The current location (the most recent one acquired, regardless of accuracy level), or nil if no valid location was acquired
188
+ - The achieved accuracy for the current location (may be less than the desired accuracy if the request failed)
189
+ - The request status (if it succeeded, or if not, why it failed)
190
+
191
+ @return The location request ID, which can be used to force early completion or cancel the request while it is in progress.
192
+ */
193
+ - (INTULocationRequestID)requestLocationWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy
194
+ timeout:(NSTimeInterval)timeout
195
+ delayUntilAuthorized:(BOOL)delayUntilAuthorized
196
+ block:(INTULocationRequestBlock)block
197
+ {
198
+ NSAssert([NSThread isMainThread], @"INTULocationManager should only be called from the main thread.");
199
+
200
+ if (desiredAccuracy == INTULocationAccuracyNone) {
201
+ NSAssert(desiredAccuracy != INTULocationAccuracyNone, @"INTULocationAccuracyNone is not a valid desired accuracy.");
202
+ desiredAccuracy = INTULocationAccuracyCity; // default to the lowest valid desired accuracy
203
+ }
204
+
205
+ INTULocationRequest *locationRequest = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSingle];
206
+ locationRequest.delegate = self;
207
+ locationRequest.desiredAccuracy = desiredAccuracy;
208
+ locationRequest.timeout = timeout;
209
+ locationRequest.block = block;
210
+
211
+ BOOL deferTimeout = delayUntilAuthorized && ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined);
212
+ if (!deferTimeout) {
213
+ [locationRequest startTimeoutTimerIfNeeded];
214
+ }
215
+
216
+ [self addLocationRequest:locationRequest];
217
+
218
+ return locationRequest.requestID;
219
+ }
220
+
221
+ /**
222
+ Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location.
223
+ This method instructs location services to use the highest accuracy available (which also requires the most power).
224
+ If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically.
225
+
226
+ @param block The block to execute every time an updated location is available.
227
+ The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut.
228
+
229
+ @return The location request ID, which can be used to cancel the subscription of location updates to this block.
230
+ */
231
+ - (INTULocationRequestID)subscribeToLocationUpdatesWithBlock:(INTULocationRequestBlock)block
232
+ {
233
+ return [self subscribeToLocationUpdatesWithDesiredAccuracy:INTULocationAccuracyRoom
234
+ block:block];
235
+ }
236
+
237
+ /**
238
+ Creates a subscription for location updates that will execute the block once per update indefinitely (until canceled), regardless of the accuracy of each location.
239
+ The specified desired accuracy is passed along to location services, and controls how much power is used, with higher accuracies using more power.
240
+ If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically.
241
+
242
+ @param desiredAccuracy The accuracy level desired, which controls how much power is used by the device's location services.
243
+ @param block The block to execute every time an updated location is available. Note that this block runs for every update, regardless of
244
+ whether the achievedAccuracy is at least the desiredAccuracy.
245
+ The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut.
246
+
247
+ @return The location request ID, which can be used to cancel the subscription of location updates to this block.
248
+ */
249
+ - (INTULocationRequestID)subscribeToLocationUpdatesWithDesiredAccuracy:(INTULocationAccuracy)desiredAccuracy
250
+ block:(INTULocationRequestBlock)block
251
+ {
252
+ NSAssert([NSThread isMainThread], @"INTULocationManager should only be called from the main thread.");
253
+
254
+ INTULocationRequest *locationRequest = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSubscription];
255
+ locationRequest.desiredAccuracy = desiredAccuracy;
256
+ locationRequest.block = block;
257
+
258
+ [self addLocationRequest:locationRequest];
259
+
260
+ return locationRequest.requestID;
261
+ }
262
+
263
+ /**
264
+ Creates a subscription for significant location changes that will execute the block once per change indefinitely (until canceled).
265
+ If an error occurs, the block will execute with a status other than INTULocationStatusSuccess, and the subscription will be canceled automatically.
266
+
267
+ @param block The block to execute every time an updated location is available.
268
+ The status will be INTULocationStatusSuccess unless an error occurred; it will never be INTULocationStatusTimedOut.
269
+
270
+ @return The location request ID, which can be used to cancel the subscription of significant location changes to this block.
271
+ */
272
+ - (INTULocationRequestID)subscribeToSignificantLocationChangesWithBlock:(INTULocationRequestBlock)block
273
+ {
274
+ NSAssert([NSThread isMainThread], @"INTULocationManager should only be called from the main thread.");
275
+
276
+ INTULocationRequest *locationRequest = [[INTULocationRequest alloc] initWithType:INTULocationRequestTypeSignificantChanges];
277
+ locationRequest.block = block;
278
+
279
+ [self addLocationRequest:locationRequest];
280
+
281
+ return locationRequest.requestID;
282
+ }
283
+
284
+ /**
285
+ Immediately forces completion of the location request with the given requestID (if it exists), and executes the original request block with the results.
286
+ This is effectively a manual timeout, and will result in the request completing with status INTULocationStatusTimedOut.
287
+ */
288
+ - (void)forceCompleteLocationRequest:(INTULocationRequestID)requestID
289
+ {
290
+ NSAssert([NSThread isMainThread], @"INTULocationManager should only be called from the main thread.");
291
+
292
+ for (INTULocationRequest *locationRequest in self.locationRequests) {
293
+ if (locationRequest.requestID == requestID) {
294
+ if (locationRequest.isRecurring) {
295
+ // Recurring requests can only be canceled
296
+ [self cancelLocationRequest:requestID];
297
+ } else {
298
+ [locationRequest forceTimeout];
299
+ [self completeLocationRequest:locationRequest];
300
+ }
301
+ break;
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ Immediately cancels the location request with the given requestID (if it exists), without executing the original request block.
308
+ */
309
+ - (void)cancelLocationRequest:(INTULocationRequestID)requestID
310
+ {
311
+ NSAssert([NSThread isMainThread], @"INTULocationManager should only be called from the main thread.");
312
+
313
+ for (INTULocationRequest *locationRequest in self.locationRequests) {
314
+ if (locationRequest.requestID == requestID) {
315
+ [locationRequest cancel];
316
+ INTULMLog(@"Location Request canceled with ID: %ld", (long)locationRequest.requestID);
317
+ [self removeLocationRequest:locationRequest];
318
+ break;
319
+ }
320
+ }
321
+ }
322
+
323
+ #pragma mark Public heading methods
324
+
325
+ /**
326
+ Asynchronously requests the current heading of the device using location services.
327
+
328
+ @param block The block to be executed when the request succeeds. One parameter is passed into the block:
329
+ - The current heading (the most recent one acquired, regardless of accuracy level), or nil if no valid heading was acquired
330
+
331
+ @return The heading request ID, which can be used remove the request from being called in the future.
332
+ */
333
+ - (INTUHeadingRequestID)subscribeToHeadingUpdatesWithBlock:(INTUHeadingRequestBlock)block
334
+ {
335
+ INTUHeadingRequest *headingRequest = [[INTUHeadingRequest alloc] init];
336
+ headingRequest.block = block;
337
+
338
+ [self addHeadingRequest:headingRequest];
339
+
340
+ return headingRequest.requestID;
341
+ }
342
+
343
+ /**
344
+ Immediately cancels the heading request with the given requestID (if it exists), without executing the original request block.
345
+ */
346
+ - (void)cancelHeadingRequest:(INTUHeadingRequestID)requestID
347
+ {
348
+ for (INTUHeadingRequest *headingRequest in self.headingRequests) {
349
+ if (headingRequest.requestID == requestID) {
350
+ [self removeHeadingRequest:headingRequest];
351
+ INTULMLog(@"Heading Request canceled with ID: %ld", (long)headingRequest.requestID);
352
+ break;
353
+ }
354
+ }
355
+ }
356
+
357
+ #pragma mark Internal location methods
358
+
359
+ /**
360
+ Adds the given location request to the array of requests, updates the maximum desired accuracy, and starts location updates if needed.
361
+ */
362
+ - (void)addLocationRequest:(INTULocationRequest *)locationRequest
363
+ {
364
+ INTULocationServicesState locationServicesState = [INTULocationManager locationServicesState];
365
+ if (locationServicesState == INTULocationServicesStateDisabled ||
366
+ locationServicesState == INTULocationServicesStateDenied ||
367
+ locationServicesState == INTULocationServicesStateRestricted) {
368
+ // No need to add this location request, because location services are turned off device-wide, or the user has denied this app permissions to use them
369
+ [self completeLocationRequest:locationRequest];
370
+ return;
371
+ }
372
+
373
+ switch (locationRequest.type) {
374
+ case INTULocationRequestTypeSingle:
375
+ case INTULocationRequestTypeSubscription:
376
+ {
377
+ INTULocationAccuracy maximumDesiredAccuracy = INTULocationAccuracyNone;
378
+ // Determine the maximum desired accuracy for all existing location requests (does not include the new request we're currently adding)
379
+ for (INTULocationRequest *locationRequest in [self activeLocationRequestsExcludingType:INTULocationRequestTypeSignificantChanges]) {
380
+ if (locationRequest.desiredAccuracy > maximumDesiredAccuracy) {
381
+ maximumDesiredAccuracy = locationRequest.desiredAccuracy;
382
+ }
383
+ }
384
+ // Take the max of the maximum desired accuracy for all existing location requests and the desired accuracy of the new request we're currently adding
385
+ maximumDesiredAccuracy = MAX(locationRequest.desiredAccuracy, maximumDesiredAccuracy);
386
+ [self updateWithMaximumDesiredAccuracy:maximumDesiredAccuracy];
387
+
388
+ [self startUpdatingLocationIfNeeded];
389
+ }
390
+ break;
391
+ case INTULocationRequestTypeSignificantChanges:
392
+ [self startMonitoringSignificantLocationChangesIfNeeded];
393
+ break;
394
+ }
395
+ __INTU_GENERICS(NSMutableArray, INTULocationRequest *) *newLocationRequests = [NSMutableArray arrayWithArray:self.locationRequests];
396
+ [newLocationRequests addObject:locationRequest];
397
+ self.locationRequests = newLocationRequests;
398
+ INTULMLog(@"Location Request added with ID: %ld", (long)locationRequest.requestID);
399
+
400
+ // Process all location requests now, as we may be able to immediately complete the request just added above
401
+ // if a location update was recently received (stored in self.currentLocation) that satisfies its criteria.
402
+ [self processLocationRequests];
403
+ }
404
+
405
+ /**
406
+ Removes a given location request from the array of requests, updates the maximum desired accuracy, and stops location updates if needed.
407
+ */
408
+ - (void)removeLocationRequest:(INTULocationRequest *)locationRequest
409
+ {
410
+ __INTU_GENERICS(NSMutableArray, INTULocationRequest *) *newLocationRequests = [NSMutableArray arrayWithArray:self.locationRequests];
411
+ [newLocationRequests removeObject:locationRequest];
412
+ self.locationRequests = newLocationRequests;
413
+
414
+ switch (locationRequest.type) {
415
+ case INTULocationRequestTypeSingle:
416
+ case INTULocationRequestTypeSubscription:
417
+ {
418
+ // Determine the maximum desired accuracy for all remaining location requests
419
+ INTULocationAccuracy maximumDesiredAccuracy = INTULocationAccuracyNone;
420
+ for (INTULocationRequest *locationRequest in [self activeLocationRequestsExcludingType:INTULocationRequestTypeSignificantChanges]) {
421
+ if (locationRequest.desiredAccuracy > maximumDesiredAccuracy) {
422
+ maximumDesiredAccuracy = locationRequest.desiredAccuracy;
423
+ }
424
+ }
425
+ [self updateWithMaximumDesiredAccuracy:maximumDesiredAccuracy];
426
+
427
+ [self stopUpdatingLocationIfPossible];
428
+ }
429
+ break;
430
+ case INTULocationRequestTypeSignificantChanges:
431
+ [self stopMonitoringSignificantLocationChangesIfPossible];
432
+ break;
433
+ }
434
+ }
435
+
436
+ /**
437
+ Returns the most recent current location, or nil if the current location is unknown, invalid, or stale.
438
+ */
439
+ - (CLLocation *)currentLocation
440
+ {
441
+ if (_currentLocation) {
442
+ // Location isn't nil, so test to see if it is valid
443
+ if (!CLLocationCoordinate2DIsValid(_currentLocation.coordinate) || (_currentLocation.coordinate.latitude == 0.0 && _currentLocation.coordinate.longitude == 0.0)) {
444
+ // The current location is invalid; discard it and return nil
445
+ _currentLocation = nil;
446
+ }
447
+ }
448
+
449
+ // Location is either nil or valid at this point, return it
450
+ return _currentLocation;
451
+ }
452
+
453
+ /**
454
+ Requests permission to use location services on devices with iOS 8+.
455
+ */
456
+ - (void)requestAuthorizationIfNeeded
457
+ {
458
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_1
459
+ // As of iOS 8, apps must explicitly request location services permissions. INTULocationManager supports both levels, "Always" and "When In Use".
460
+ // INTULocationManager determines which level of permissions to request based on which description key is present in your app's Info.plist
461
+ // If you provide values for both description keys, the more permissive "Always" level is requested.
462
+
463
+ double iOSVersion = floor(NSFoundationVersionNumber);
464
+ BOOL isiOSVersion7to10 = iOSVersion > NSFoundationVersionNumber_iOS_7_1 && iOSVersion <= NSFoundationVersionNumber10_11_Max;
465
+ if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) {
466
+ if (isiOSVersion7to10) {
467
+ BOOL hasAlwaysKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"] != nil;
468
+ BOOL hasWhenInUseKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] != nil;
469
+ if (hasAlwaysKey) {
470
+ [self.locationManager requestAlwaysAuthorization];
471
+ } else if (hasWhenInUseKey) {
472
+ [self.locationManager requestWhenInUseAuthorization];
473
+ } else {
474
+ // At least one of the keys NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription MUST be present in the Info.plist file to use location services on iOS 8+.
475
+ NSAssert(hasAlwaysKey || hasWhenInUseKey, @"To use location services in iOS 8+, your Info.plist must provide a value for either NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription.");
476
+ }
477
+ } else {
478
+ BOOL hasAlwaysAndInUseKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"] != nil;
479
+ BOOL hasWhenInUseKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] != nil;
480
+ if (hasAlwaysAndInUseKey) {
481
+ [self.locationManager requestAlwaysAuthorization];
482
+ } else if (hasWhenInUseKey) {
483
+ [self.locationManager requestWhenInUseAuthorization];
484
+ } else {
485
+ // Key NSLocationAlwaysAndWhenInUseUsageDescription MUST be present in the Info.plist file to use location services on iOS 11+.
486
+ NSAssert(hasAlwaysAndInUseKey, @"To use location services in iOS 11+, your Info.plist must provide a value for NSLocationAlwaysAndWhenInUseUsageDescription.");
487
+ }
488
+ }
489
+ }
490
+ #endif /* __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_1 */
491
+ }
492
+
493
+ /**
494
+ Sets the CLLocationManager desiredAccuracy based on the given maximum desired accuracy (which should be the maximum desired accuracy of all active location requests).
495
+ */
496
+ - (void)updateWithMaximumDesiredAccuracy:(INTULocationAccuracy)maximumDesiredAccuracy
497
+ {
498
+ switch (maximumDesiredAccuracy) {
499
+ case INTULocationAccuracyNone:
500
+ break;
501
+ case INTULocationAccuracyCity:
502
+ if (self.locationManager.desiredAccuracy != kCLLocationAccuracyThreeKilometers) {
503
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
504
+ INTULMLog(@"Changing location services accuracy level to: low (minimum).");
505
+ }
506
+ break;
507
+ case INTULocationAccuracyNeighborhood:
508
+ if (self.locationManager.desiredAccuracy != kCLLocationAccuracyKilometer) {
509
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
510
+ INTULMLog(@"Changing location services accuracy level to: medium low.");
511
+ }
512
+ break;
513
+ case INTULocationAccuracyBlock:
514
+ if (self.locationManager.desiredAccuracy != kCLLocationAccuracyHundredMeters) {
515
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
516
+ INTULMLog(@"Changing location services accuracy level to: medium.");
517
+ }
518
+ break;
519
+ case INTULocationAccuracyHouse:
520
+ if (self.locationManager.desiredAccuracy != kCLLocationAccuracyNearestTenMeters) {
521
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
522
+ INTULMLog(@"Changing location services accuracy level to: medium high.");
523
+ }
524
+ break;
525
+ case INTULocationAccuracyRoom:
526
+ if (self.locationManager.desiredAccuracy != kCLLocationAccuracyBest) {
527
+ self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
528
+ INTULMLog(@"Changing location services accuracy level to: high (maximum).");
529
+ }
530
+ break;
531
+ default:
532
+ NSAssert(nil, @"Invalid maximum desired accuracy!");
533
+ break;
534
+ }
535
+ }
536
+
537
+ /**
538
+ Inform CLLocationManager to start monitoring significant location changes.
539
+ */
540
+ - (void)startMonitoringSignificantLocationChangesIfNeeded
541
+ {
542
+ [self requestAuthorizationIfNeeded];
543
+
544
+ NSArray *locationRequests = [self activeLocationRequestsWithType:INTULocationRequestTypeSignificantChanges];
545
+ if (locationRequests.count == 0) {
546
+ [self.locationManager startMonitoringSignificantLocationChanges];
547
+ if (self.isMonitoringSignificantLocationChanges == NO) {
548
+ INTULMLog(@"Significant location change monitoring has started.")
549
+ }
550
+ self.isMonitoringSignificantLocationChanges = YES;
551
+ }
552
+ }
553
+
554
+ /**
555
+ Inform CLLocationManager to start sending us updates to our location.
556
+ */
557
+ - (void)startUpdatingLocationIfNeeded
558
+ {
559
+ [self requestAuthorizationIfNeeded];
560
+
561
+ NSArray *locationRequests = [self activeLocationRequestsExcludingType:INTULocationRequestTypeSignificantChanges];
562
+ if (locationRequests.count == 0) {
563
+ [self.locationManager startUpdatingLocation];
564
+ if (self.isUpdatingLocation == NO) {
565
+ INTULMLog(@"Location services updates have started.");
566
+ }
567
+ self.isUpdatingLocation = YES;
568
+ }
569
+ }
570
+
571
+ - (void)stopMonitoringSignificantLocationChangesIfPossible
572
+ {
573
+ NSArray *locationRequests = [self activeLocationRequestsWithType:INTULocationRequestTypeSignificantChanges];
574
+ if (locationRequests.count == 0) {
575
+ [self.locationManager stopMonitoringSignificantLocationChanges];
576
+ if (self.isMonitoringSignificantLocationChanges) {
577
+ INTULMLog(@"Significant location change monitoring has stopped.");
578
+ }
579
+ self.isMonitoringSignificantLocationChanges = NO;
580
+ }
581
+ }
582
+
583
+ /**
584
+ Checks to see if there are any outstanding locationRequests, and if there are none, informs CLLocationManager to stop sending
585
+ location updates. This is done as soon as location updates are no longer needed in order to conserve the device's battery.
586
+ */
587
+ - (void)stopUpdatingLocationIfPossible
588
+ {
589
+ NSArray *locationRequests = [self activeLocationRequestsExcludingType:INTULocationRequestTypeSignificantChanges];
590
+ if (locationRequests.count == 0) {
591
+ [self.locationManager stopUpdatingLocation];
592
+ if (self.isUpdatingLocation) {
593
+ INTULMLog(@"Location services updates have stopped.");
594
+ }
595
+ self.isUpdatingLocation = NO;
596
+ }
597
+ }
598
+
599
+ /**
600
+ Iterates over the array of active location requests to check and see if the most recent current location
601
+ successfully satisfies any of their criteria.
602
+ */
603
+ - (void)processLocationRequests
604
+ {
605
+ CLLocation *mostRecentLocation = self.currentLocation;
606
+
607
+ for (INTULocationRequest *locationRequest in self.locationRequests) {
608
+ if (locationRequest.hasTimedOut) {
609
+ // Non-recurring request has timed out, complete it
610
+ [self completeLocationRequest:locationRequest];
611
+ continue;
612
+ }
613
+
614
+ if (mostRecentLocation != nil) {
615
+ if (locationRequest.isRecurring) {
616
+ // This is a subscription request, which lives indefinitely (unless manually canceled) and receives every location update we get
617
+ [self processRecurringRequest:locationRequest];
618
+ continue;
619
+ } else {
620
+ // This is a regular one-time location request
621
+ NSTimeInterval currentLocationTimeSinceUpdate = fabs([mostRecentLocation.timestamp timeIntervalSinceNow]);
622
+ CLLocationAccuracy currentLocationHorizontalAccuracy = mostRecentLocation.horizontalAccuracy;
623
+ NSTimeInterval staleThreshold = [locationRequest updateTimeStaleThreshold];
624
+ CLLocationAccuracy horizontalAccuracyThreshold = [locationRequest horizontalAccuracyThreshold];
625
+ if (currentLocationTimeSinceUpdate <= staleThreshold &&
626
+ currentLocationHorizontalAccuracy <= horizontalAccuracyThreshold) {
627
+ // The request's desired accuracy has been reached, complete it
628
+ [self completeLocationRequest:locationRequest];
629
+ continue;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ /**
637
+ Immediately completes all active location requests.
638
+ Used in cases such as when the location services authorization status changes to Denied or Restricted.
639
+ */
640
+ - (void)completeAllLocationRequests
641
+ {
642
+ // Iterate through a copy of the locationRequests array to avoid modifying the same array we are removing elements from
643
+ __INTU_GENERICS(NSArray, INTULocationRequest *) *locationRequests = [self.locationRequests copy];
644
+ for (INTULocationRequest *locationRequest in locationRequests) {
645
+ [self completeLocationRequest:locationRequest];
646
+ }
647
+ INTULMLog(@"Finished completing all location requests.");
648
+ }
649
+
650
+ /**
651
+ Completes the given location request by removing it from the array of locationRequests and executing its completion block.
652
+ */
653
+ - (void)completeLocationRequest:(INTULocationRequest *)locationRequest
654
+ {
655
+ if (locationRequest == nil) {
656
+ return;
657
+ }
658
+
659
+ [locationRequest complete];
660
+ [self removeLocationRequest:locationRequest];
661
+
662
+ INTULocationStatus status = [self statusForLocationRequest:locationRequest];
663
+ CLLocation *currentLocation = self.currentLocation;
664
+ INTULocationAccuracy achievedAccuracy = [self achievedAccuracyForLocation:currentLocation];
665
+
666
+ // INTULocationManager is not thread safe and should only be called from the main thread, so we should already be executing on the main thread now.
667
+ // dispatch_async is used to ensure that the completion block for a request is not executed before the request ID is returned, for example in the
668
+ // case where the user has denied permission to access location services and the request is immediately completed with the appropriate error.
669
+ dispatch_async(dispatch_get_main_queue(), ^{
670
+ if (locationRequest.block) {
671
+ locationRequest.block(currentLocation, achievedAccuracy, status);
672
+ }
673
+ });
674
+
675
+ INTULMLog(@"Location Request completed with ID: %ld, currentLocation: %@, achievedAccuracy: %lu, status: %lu", (long)locationRequest.requestID, currentLocation, (unsigned long) achievedAccuracy, (unsigned long)status);
676
+ }
677
+
678
+ /**
679
+ Handles calling a recurring location request's block with the current location.
680
+ */
681
+ - (void)processRecurringRequest:(INTULocationRequest *)locationRequest
682
+ {
683
+ NSAssert(locationRequest.isRecurring, @"This method should only be called for recurring location requests.");
684
+
685
+ INTULocationStatus status = [self statusForLocationRequest:locationRequest];
686
+ CLLocation *currentLocation = self.currentLocation;
687
+ INTULocationAccuracy achievedAccuracy = [self achievedAccuracyForLocation:currentLocation];
688
+
689
+ // INTULocationManager is not thread safe and should only be called from the main thread, so we should already be executing on the main thread now.
690
+ // dispatch_async is used to ensure that the completion block for a request is not executed before the request ID is returned.
691
+ dispatch_async(dispatch_get_main_queue(), ^{
692
+ if (locationRequest.block) {
693
+ locationRequest.block(currentLocation, achievedAccuracy, status);
694
+ }
695
+ });
696
+ }
697
+
698
+ /**
699
+ Returns all active location requests with the given type.
700
+ */
701
+ - (NSArray *)activeLocationRequestsWithType:(INTULocationRequestType)locationRequestType
702
+ {
703
+ return [self.locationRequests filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(INTULocationRequest *evaluatedObject, NSDictionary *bindings) {
704
+ return evaluatedObject.type == locationRequestType;
705
+ }]];
706
+ }
707
+
708
+ /**
709
+ Returns all active location requests excluding requests with the given type.
710
+ */
711
+ - (NSArray *)activeLocationRequestsExcludingType:(INTULocationRequestType)locationRequestType
712
+ {
713
+ return [self.locationRequests filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(INTULocationRequest *evaluatedObject, NSDictionary *bindings) {
714
+ return evaluatedObject.type != locationRequestType;
715
+ }]];
716
+ }
717
+
718
+ /**
719
+ Returns the location manager status for the given location request.
720
+ */
721
+ - (INTULocationStatus)statusForLocationRequest:(INTULocationRequest *)locationRequest
722
+ {
723
+ INTULocationServicesState locationServicesState = [INTULocationManager locationServicesState];
724
+
725
+ if (locationServicesState == INTULocationServicesStateDisabled) {
726
+ return INTULocationStatusServicesDisabled;
727
+ }
728
+ else if (locationServicesState == INTULocationServicesStateNotDetermined) {
729
+ return INTULocationStatusServicesNotDetermined;
730
+ }
731
+ else if (locationServicesState == INTULocationServicesStateDenied) {
732
+ return INTULocationStatusServicesDenied;
733
+ }
734
+ else if (locationServicesState == INTULocationServicesStateRestricted) {
735
+ return INTULocationStatusServicesRestricted;
736
+ }
737
+ else if (self.updateFailed) {
738
+ return INTULocationStatusError;
739
+ }
740
+ else if (locationRequest.hasTimedOut) {
741
+ return INTULocationStatusTimedOut;
742
+ }
743
+
744
+ return INTULocationStatusSuccess;
745
+ }
746
+
747
+ /**
748
+ Returns the associated INTULocationAccuracy level that has been achieved for a given location,
749
+ based on that location's horizontal accuracy and recency.
750
+ */
751
+ - (INTULocationAccuracy)achievedAccuracyForLocation:(CLLocation *)location
752
+ {
753
+ if (!location) {
754
+ return INTULocationAccuracyNone;
755
+ }
756
+
757
+ NSTimeInterval timeSinceUpdate = fabs([location.timestamp timeIntervalSinceNow]);
758
+ CLLocationAccuracy horizontalAccuracy = location.horizontalAccuracy;
759
+
760
+ if (horizontalAccuracy <= kINTUHorizontalAccuracyThresholdRoom &&
761
+ timeSinceUpdate <= kINTUUpdateTimeStaleThresholdRoom) {
762
+ return INTULocationAccuracyRoom;
763
+ }
764
+ else if (horizontalAccuracy <= kINTUHorizontalAccuracyThresholdHouse &&
765
+ timeSinceUpdate <= kINTUUpdateTimeStaleThresholdHouse) {
766
+ return INTULocationAccuracyHouse;
767
+ }
768
+ else if (horizontalAccuracy <= kINTUHorizontalAccuracyThresholdBlock &&
769
+ timeSinceUpdate <= kINTUUpdateTimeStaleThresholdBlock) {
770
+ return INTULocationAccuracyBlock;
771
+ }
772
+ else if (horizontalAccuracy <= kINTUHorizontalAccuracyThresholdNeighborhood &&
773
+ timeSinceUpdate <= kINTUUpdateTimeStaleThresholdNeighborhood) {
774
+ return INTULocationAccuracyNeighborhood;
775
+ }
776
+ else if (horizontalAccuracy <= kINTUHorizontalAccuracyThresholdCity &&
777
+ timeSinceUpdate <= kINTUUpdateTimeStaleThresholdCity) {
778
+ return INTULocationAccuracyCity;
779
+ }
780
+ else {
781
+ return INTULocationAccuracyNone;
782
+ }
783
+ }
784
+
785
+ #pragma mark Internal heading methods
786
+
787
+ /**
788
+ Returns the most recent heading, or nil if the current heading is unknown or invalid.
789
+ */
790
+ - (CLHeading *)currentHeading
791
+ {
792
+ // Heading isn't nil, so test to see if it is valid
793
+ if (!INTUCLHeadingIsIsValid(_currentHeading)) {
794
+ // The current heading is invalid; discard it and return nil
795
+ _currentHeading = nil;
796
+ }
797
+
798
+ // Heading is either nil or valid at this point, return it
799
+ return _currentHeading;
800
+ }
801
+
802
+ /**
803
+ Checks whether the given @c CLHeading has valid properties.
804
+ */
805
+ BOOL INTUCLHeadingIsIsValid(CLHeading *heading)
806
+ {
807
+ return heading.trueHeading > 0 &&
808
+ heading.headingAccuracy > 0;
809
+ }
810
+
811
+ /**
812
+ Adds the given heading request to the array of requests and starts heading updates.
813
+ */
814
+ - (void)addHeadingRequest:(INTUHeadingRequest *)headingRequest
815
+ {
816
+ NSAssert(headingRequest, @"Must pass in a non-nil heading request.");
817
+
818
+ // If heading services are not available, just return
819
+ if ([INTULocationManager headingServicesState] == INTUHeadingServicesStateUnavailable) {
820
+ // dispatch_async is used to ensure that the completion block for a request is not executed before the request ID is returned.
821
+ dispatch_async(dispatch_get_main_queue(), ^{
822
+ if (headingRequest.block) {
823
+ headingRequest.block(nil, INTUHeadingStatusUnavailable);
824
+ }
825
+ });
826
+ INTULMLog(@"Heading Request (ID %ld) NOT added since device heading is unavailable.", (long)headingRequest.requestID);
827
+ return;
828
+ }
829
+
830
+ __INTU_GENERICS(NSMutableArray, INTUHeadingRequest *) *newHeadingRequests = [NSMutableArray arrayWithArray:self.headingRequests];
831
+ [newHeadingRequests addObject:headingRequest];
832
+ self.headingRequests = newHeadingRequests;
833
+ INTULMLog(@"Heading Request added with ID: %ld", (long)headingRequest.requestID);
834
+
835
+ [self startUpdatingHeadingIfNeeded];
836
+ }
837
+
838
+ /**
839
+ Inform CLLocationManager to start sending us updates to our heading.
840
+ */
841
+ - (void)startUpdatingHeadingIfNeeded
842
+ {
843
+ if (self.headingRequests.count != 0) {
844
+ [self.locationManager startUpdatingHeading];
845
+ if (self.isUpdatingHeading == NO) {
846
+ INTULMLog(@"Heading services updates have started.");
847
+ }
848
+ self.isUpdatingHeading = YES;
849
+ }
850
+ }
851
+
852
+ /**
853
+ Removes a given heading request from the array of requests and stops heading updates if needed.
854
+ */
855
+ - (void)removeHeadingRequest:(INTUHeadingRequest *)headingRequest
856
+ {
857
+ __INTU_GENERICS(NSMutableArray, INTUHeadingRequest *) *newHeadingRequests = [NSMutableArray arrayWithArray:self.headingRequests];
858
+ [newHeadingRequests removeObject:headingRequest];
859
+ self.headingRequests = newHeadingRequests;
860
+
861
+ [self stopUpdatingHeadingIfPossible];
862
+ }
863
+
864
+ /**
865
+ Checks to see if there are any outstanding headingRequests, and if there are none, informs CLLocationManager to stop sending
866
+ heading updates. This is done as soon as heading updates are no longer needed in order to conserve the device's battery.
867
+ */
868
+ - (void)stopUpdatingHeadingIfPossible
869
+ {
870
+ if (self.headingRequests.count == 0) {
871
+ [self.locationManager stopUpdatingHeading];
872
+ if (self.isUpdatingHeading) {
873
+ INTULMLog(@"Location services heading updates have stopped.");
874
+ }
875
+ self.isUpdatingHeading = NO;
876
+ }
877
+ }
878
+
879
+ /**
880
+ Iterates over the array of active heading requests and processes each
881
+ */
882
+ - (void)processRecurringHeadingRequests
883
+ {
884
+ for (INTUHeadingRequest *headingRequest in self.headingRequests) {
885
+ [self processRecurringHeadingRequest:headingRequest];
886
+ }
887
+ }
888
+
889
+ /**
890
+ Handles calling a recurring heading request's block with the current heading.
891
+ */
892
+ - (void)processRecurringHeadingRequest:(INTUHeadingRequest *)headingRequest
893
+ {
894
+ NSAssert(headingRequest.isRecurring, @"This method should only be called for recurring heading requests.");
895
+
896
+ INTUHeadingStatus status = [self statusForHeadingRequest:headingRequest];
897
+
898
+ // Check if the request had a fatal error and should be canceled
899
+ if (status == INTUHeadingStatusUnavailable) {
900
+ // dispatch_async is used to ensure that the completion block for a request is not executed before the request ID is returned.
901
+ dispatch_async(dispatch_get_main_queue(), ^{
902
+ if (headingRequest.block) {
903
+ headingRequest.block(nil, status);
904
+ }
905
+ });
906
+
907
+ [self cancelHeadingRequest:headingRequest.requestID];
908
+ return;
909
+ }
910
+
911
+ // dispatch_async is used to ensure that the completion block for a request is not executed before the request ID is returned.
912
+ dispatch_async(dispatch_get_main_queue(), ^{
913
+ if (headingRequest.block) {
914
+ headingRequest.block(self.currentHeading, status);
915
+ }
916
+ });
917
+ }
918
+
919
+ /**
920
+ Returns the status for the given heading request.
921
+ */
922
+ - (INTUHeadingStatus)statusForHeadingRequest:(INTUHeadingRequest *)headingRequest
923
+ {
924
+ if ([INTULocationManager headingServicesState] == INTUHeadingServicesStateUnavailable) {
925
+ return INTUHeadingStatusUnavailable;
926
+ }
927
+
928
+ // The accessor will return nil for an invalid heading results
929
+ if (!self.currentHeading) {
930
+ return INTUHeadingStatusInvalid;
931
+ }
932
+
933
+ return INTUHeadingStatusSuccess;
934
+ }
935
+
936
+ #pragma mark INTULocationRequestDelegate method
937
+
938
+ - (void)locationRequestDidTimeout:(INTULocationRequest *)locationRequest
939
+ {
940
+ // For robustness, only complete the location request if it is still active (by checking to see that it hasn't been removed from the locationRequests array).
941
+ for (INTULocationRequest *activeLocationRequest in self.locationRequests) {
942
+ if (activeLocationRequest.requestID == locationRequest.requestID) {
943
+ [self completeLocationRequest:locationRequest];
944
+ break;
945
+ }
946
+ }
947
+ }
948
+
949
+ #pragma mark CLLocationManagerDelegate methods
950
+
951
+ - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
952
+ {
953
+ // Received update successfully, so clear any previous errors
954
+ self.updateFailed = NO;
955
+
956
+ CLLocation *mostRecentLocation = [locations lastObject];
957
+ self.currentLocation = mostRecentLocation;
958
+
959
+ // Process the location requests using the updated location
960
+ [self processLocationRequests];
961
+ }
962
+
963
+ - (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
964
+ {
965
+ self.currentHeading = newHeading;
966
+
967
+ // Process the heading requests using the updated heading
968
+ [self processRecurringHeadingRequests];
969
+ }
970
+
971
+ - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
972
+ {
973
+ INTULMLog(@"Location services error: %@", [error localizedDescription]);
974
+ self.updateFailed = YES;
975
+
976
+ for (INTULocationRequest *locationRequest in self.locationRequests) {
977
+ if (locationRequest.isRecurring) {
978
+ // Keep the recurring request alive
979
+ [self processRecurringRequest:locationRequest];
980
+ } else {
981
+ // Fail any non-recurring requests
982
+ [self completeLocationRequest:locationRequest];
983
+ }
984
+ }
985
+ }
986
+
987
+ - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
988
+ {
989
+ if (status == kCLAuthorizationStatusDenied || status == kCLAuthorizationStatusRestricted) {
990
+ // Clear out any active location requests (which will execute the blocks with a status that reflects
991
+ // the unavailability of location services) since we now no longer have location services permissions
992
+ [self completeAllLocationRequests];
993
+ }
994
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_1
995
+ else if (status == kCLAuthorizationStatusAuthorizedAlways || status == kCLAuthorizationStatusAuthorizedWhenInUse) {
996
+ #else
997
+ else if (status == kCLAuthorizationStatusAuthorized) {
998
+ #endif /* __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_7_1 */
999
+
1000
+ // Start the timeout timer for location requests that were waiting for authorization
1001
+ for (INTULocationRequest *locationRequest in self.locationRequests) {
1002
+ [locationRequest startTimeoutTimerIfNeeded];
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ #pragma mark - Additions
1008
+ /** It is possible to force enable background location fetch even if your set any kind of Authorizations */
1009
+ - (void)setBackgroundLocationUpdate:(BOOL) enabled {
1010
+ if (@available(iOS 9, *)) {
1011
+ _locationManager.allowsBackgroundLocationUpdates = enabled;
1012
+ }
1013
+ }
1014
+
1015
+ - (void)setShowsBackgroundLocationIndicator:(BOOL) shows {
1016
+ if (@available(iOS 11, *)) {
1017
+ _locationManager.showsBackgroundLocationIndicator = shows;
1018
+ }
1019
+ }
1020
+
1021
+ - (void)setPausesLocationUpdatesAutomatically:(BOOL) pauses
1022
+ {
1023
+ _locationManager.pausesLocationUpdatesAutomatically = pauses;
1024
+ }
1025
+ @end