@@ -4,17 +4,24 @@ import android.app.Activity
44import android.content.Context
55import android.location.Location
66import android.location.LocationManager
7- import android.os.Build
7+ import android.net.ConnectivityManager
88import android.util.Log
99import androidx.activity.result.ActivityResultLauncher
1010import androidx.activity.result.IntentSenderRequest
11+ import androidx.core.location.LocationListenerCompat
1112import androidx.core.location.LocationManagerCompat
1213import com.google.android.gms.location.FusedLocationProviderClient
1314import com.google.android.gms.location.LocationCallback
1415import com.google.android.gms.location.LocationResult
16+ import com.google.android.gms.location.LocationServices
17+ import io.ionic.libs.iongeolocationlib.controller.helper.IONGLOCFallbackHelper
18+ import io.ionic.libs.iongeolocationlib.controller.helper.IONGLOCGoogleServicesHelper
19+ import io.ionic.libs.iongeolocationlib.controller.helper.toOSLocationResult
1520import io.ionic.libs.iongeolocationlib.model.IONGLOCException
1621import io.ionic.libs.iongeolocationlib.model.IONGLOCLocationOptions
1722import io.ionic.libs.iongeolocationlib.model.IONGLOCLocationResult
23+ import io.ionic.libs.iongeolocationlib.model.internal.LocationHandler
24+ import io.ionic.libs.iongeolocationlib.model.internal.LocationSettingsResult
1825import kotlinx.coroutines.channels.awaitClose
1926import kotlinx.coroutines.flow.Flow
2027import kotlinx.coroutines.flow.MutableSharedFlow
@@ -25,16 +32,34 @@ import kotlinx.coroutines.flow.first
2532 * Entry point in IONGeolocationLib-Android
2633 *
2734 */
28- class IONGLOCController (
35+ class IONGLOCController internal constructor (
2936 fusedLocationClient : FusedLocationProviderClient ,
37+ private val locationManager : LocationManager ,
38+ connectivityManager : ConnectivityManager ,
3039 activityLauncher : ActivityResultLauncher <IntentSenderRequest >,
31- private val helper : IONGLOCServiceHelper = IONGLOCServiceHelper (
40+ private val googleServicesHelper : IONGLOCGoogleServicesHelper = IONGLOCGoogleServicesHelper (
41+ locationManager,
42+ connectivityManager,
3243 fusedLocationClient,
3344 activityLauncher
45+ ),
46+ private val fallbackHelper : IONGLOCFallbackHelper = IONGLOCFallbackHelper (
47+ locationManager, connectivityManager
3448 )
3549) {
50+
51+ constructor (
52+ context: Context ,
53+ activityLauncher: ActivityResultLauncher <IntentSenderRequest >
54+ ) : this (
55+ fusedLocationClient = LocationServices .getFusedLocationProviderClient(context),
56+ locationManager = context.getSystemService(Context .LOCATION_SERVICE ) as LocationManager ,
57+ connectivityManager = context.getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager ,
58+ activityLauncher = activityLauncher
59+ )
60+
3661 private lateinit var resolveLocationSettingsResultFlow: MutableSharedFlow <Result <Unit >>
37- private val locationCallbacks : MutableMap <String , LocationCallback > = mutableMapOf ()
62+ private val watchLocationHandlers : MutableMap <String , LocationHandler > = mutableMapOf ()
3863 private val watchIdsBlacklist: MutableList <String > = mutableListOf ()
3964
4065 /* *
@@ -48,20 +73,25 @@ class IONGLOCController(
4873 activity : Activity ,
4974 options : IONGLOCLocationOptions
5075 ): Result <IONGLOCLocationResult > {
51- try {
76+ return try {
5277 val checkResult: Result <Unit > =
5378 checkLocationPreconditions(activity, options, isSingleLocationRequest = true )
54- return if (checkResult.isFailure ) {
79+ if (checkResult.shouldNotProceed(options) ) {
5580 Result .failure(
5681 checkResult.exceptionOrNull() ? : NullPointerException ()
5782 )
5883 } else {
59- val location = helper.getCurrentLocation(options)
60- return Result .success(location.toOSLocationResult())
84+ val location: Location =
85+ if (checkResult.isFailure && options.enableLocationManagerFallback) {
86+ fallbackHelper.getCurrentLocation(options)
87+ } else {
88+ googleServicesHelper.getCurrentLocation(options)
89+ }
90+ Result .success(location.toOSLocationResult())
6191 }
6292 } catch (exception: Exception ) {
6393 Log .d(LOG_TAG , " Error fetching location: ${exception.message} " )
64- return Result .failure(exception)
94+ Result .failure(exception)
6595 }
6696 }
6797
@@ -86,10 +116,10 @@ class IONGLOCController(
86116
87117 /* *
88118 * Checks if location services are enabled
89- * @param context Context to use when determining if location is enabled
119+ * @return true if location is enabled, false otherwise
90120 */
91- fun areLocationServicesEnabled (context : Context ): Boolean {
92- return LocationManagerCompat .isLocationEnabled(context.getSystemService( Context . LOCATION_SERVICE ) as LocationManager )
121+ fun areLocationServicesEnabled (): Boolean {
122+ return LocationManagerCompat .isLocationEnabled(locationManager )
93123 }
94124
95125 /* *
@@ -104,28 +134,29 @@ class IONGLOCController(
104134 options : IONGLOCLocationOptions ,
105135 watchId : String
106136 ): Flow <Result <List <IONGLOCLocationResult >>> = callbackFlow {
107-
108137 try {
138+ fun onNewLocations (locations : List <Location >) {
139+ if (checkWatchInBlackList(watchId)) {
140+ return
141+ }
142+ val locationResultList = locations.map { currentLocation ->
143+ currentLocation.toOSLocationResult()
144+ }
145+ trySend(Result .success(locationResultList))
146+ }
147+
109148 val checkResult: Result <Unit > =
110- checkLocationPreconditions(activity, options, isSingleLocationRequest = true )
111- if (checkResult.isFailure ) {
149+ checkLocationPreconditions(activity, options, isSingleLocationRequest = false )
150+ if (checkResult.shouldNotProceed(options) ) {
112151 trySend(
113152 Result .failure(checkResult.exceptionOrNull() ? : NullPointerException ())
114153 )
115154 } else {
116- locationCallbacks[watchId] = object : LocationCallback () {
117- override fun onLocationResult (location : LocationResult ) {
118- if (checkWatchInBlackList(watchId)) {
119- return
120- }
121- val locations = location.locations.map { currentLocation ->
122- currentLocation.toOSLocationResult()
123- }
124- trySend(Result .success(locations))
125- }
126- }.also {
127- helper.requestLocationUpdates(options, it)
128- }
155+ requestLocationUpdates(
156+ watchId,
157+ options,
158+ useFallback = checkResult.isFailure && options.enableLocationManagerFallback
159+ ) { onNewLocations(it) }
129160 }
130161 } catch (exception: Exception ) {
131162 Log .d(LOG_TAG , " Error requesting location updates: ${exception.message} " )
@@ -163,23 +194,59 @@ class IONGLOCController(
163194 )
164195 )
165196 }
166-
167- val playServicesResult = helper.checkGooglePlayServicesAvailable(activity)
197+ // if meant to use fallback, then resolvable errors from Play Services Location don't need to be addressed
198+ val playServicesResult = googleServicesHelper.checkGooglePlayServicesAvailable(
199+ activity, shouldTryResolve = ! options.enableLocationManagerFallback
200+ )
168201 if (playServicesResult.isFailure) {
169202 return Result .failure(playServicesResult.exceptionOrNull() ? : NullPointerException ())
170203 }
171204
172205 resolveLocationSettingsResultFlow = MutableSharedFlow ()
173- val locationSettingsChecked = helper .checkLocationSettings(
206+ val locationSettingsResult = googleServicesHelper .checkLocationSettings(
174207 activity,
175- options,
176- interval = if (isSingleLocationRequest) 0 else options.timeout
208+ options.copy(timeout = if (isSingleLocationRequest) 0 else options.timeout) ,
209+ shouldTryResolve = ! options.enableLocationManagerFallback
177210 )
178211
179- return if (locationSettingsChecked) {
180- Result .success(Unit )
212+ return locationSettingsResult.toKotlinResult()
213+ }
214+
215+ /* *
216+ * Request location updates using the appropriate helper class
217+ * @param watchId a unique id to associate with the location update request (so that it may be cleared later)
218+ * @param options location request options to use
219+ * @param useFallback whether or not the fallback should be used
220+ * @param onNewLocations lambda to notify of new location requests
221+ */
222+ private fun requestLocationUpdates (
223+ watchId : String ,
224+ options : IONGLOCLocationOptions ,
225+ useFallback : Boolean ,
226+ onNewLocations : (List <Location >) -> Unit
227+ ) {
228+ watchLocationHandlers[watchId] = if (! useFallback) {
229+ LocationHandler .Callback (object : LocationCallback () {
230+ override fun onLocationResult (location : LocationResult ) {
231+ onNewLocations(location.locations)
232+ }
233+ }).also {
234+ googleServicesHelper.requestLocationUpdates(options, it.callback)
235+ }
181236 } else {
182- resolveLocationSettingsResultFlow.first()
237+ LocationHandler .Listener (object : LocationListenerCompat {
238+ override fun onLocationChanged (location : Location ) {
239+ onNewLocations(listOf (location))
240+ }
241+
242+ override fun onLocationChanged (locations : List <Location ?>) {
243+ locations.filterNotNull().takeIf { it.isNotEmpty() }?.let {
244+ onNewLocations(it)
245+ }
246+ }
247+ }).also {
248+ fallbackHelper.requestLocationUpdates(options, it.listener)
249+ }
183250 }
184251 }
185252
@@ -190,17 +257,26 @@ class IONGLOCController(
190257 * @return true if watch was cleared, false if watch was not found
191258 */
192259 private fun clearWatch (id : String , addToBlackList : Boolean ): Boolean {
193- val locationCallback = locationCallbacks.remove(key = id)
194- return if (locationCallback != null ) {
195- helper.removeLocationUpdates(locationCallback)
196- true
197- } else {
198- if (addToBlackList) {
199- // It is possible that clearWatch is being called before requestLocationUpdates is triggered (e.g. very low timeout on JavaScript side.)
200- // add to a blacklist in order to remove the location callback in the future
201- watchIdsBlacklist.add(id)
260+ val watchHandler = watchLocationHandlers.remove(key = id)
261+ return when (watchHandler) {
262+ is LocationHandler .Callback -> {
263+ googleServicesHelper.removeLocationUpdates(watchHandler.callback)
264+ true
265+ }
266+
267+ is LocationHandler .Listener -> {
268+ fallbackHelper.removeLocationUpdates(watchHandler.listener)
269+ true
270+ }
271+
272+ else -> {
273+ if (addToBlackList) {
274+ // It is possible that clearWatch is being called before requestLocationUpdates is triggered (e.g. very low timeout on JavaScript side.)
275+ // add to a blacklist in order to remove the location callback in the future
276+ watchIdsBlacklist.add(id)
277+ }
278+ false
202279 }
203- false
204280 }
205281 }
206282
@@ -223,19 +299,26 @@ class IONGLOCController(
223299 }
224300
225301 /* *
226- * Extension function to convert Location object into OSLocationResult object
227- * @return OSLocationResult object
302+ * Extension function to convert the [LocationSettingsResult].
303+ * Depending on the result value, it may suspend to await a flow
304+ * @return a regular Kotlin [Result], which may be either Success or Error.
228305 */
229- private fun Location.toOSLocationResult (): IONGLOCLocationResult = IONGLOCLocationResult (
230- latitude = this .latitude,
231- longitude = this .longitude,
232- altitude = this .altitude,
233- accuracy = this .accuracy,
234- altitudeAccuracy = if (IONGLOCBuildConfig .getAndroidSdkVersionCode() >= Build .VERSION_CODES .O ) this .verticalAccuracyMeters else null ,
235- heading = this .bearing,
236- speed = this .speed,
237- timestamp = this .time
238- )
306+ private suspend fun LocationSettingsResult.toKotlinResult (): Result <Unit > {
307+ return when (this ) {
308+ LocationSettingsResult .Success -> Result .success(Unit )
309+ LocationSettingsResult .Resolving -> resolveLocationSettingsResultFlow.first()
310+ is LocationSettingsResult .ResolveSkipped -> Result .failure(resolvableError)
311+ is LocationSettingsResult .UnresolvableError -> Result .failure(error)
312+ }
313+ }
314+
315+ /* *
316+ * @return true if the the settings result is such that the location request must fail
317+ * (even if enableLocationManagerFallback=true), or false otherwise
318+ */
319+ private fun Result<Unit>.shouldNotProceed (options : IONGLOCLocationOptions ): Boolean =
320+ isFailure && (! options.enableLocationManagerFallback ||
321+ exceptionOrNull() is IONGLOCException .IONGLOCLocationAndNetworkDisabledException )
239322
240323 companion object {
241324 private const val LOG_TAG = " IONGeolocationController"
0 commit comments