I want to get the current GPS location of the device using LocationManager.requestSingleUpdate(). The following code is used to wrap the call to LocationManager in a kotlin suspending function:
private var locationManager =
context.getSystemService(LOCATION_SERVICE) as LocationManager
#RequiresPermission("android.permission.ACCESS_FINE_LOCATION")
suspend fun getCurrentLocationPreS(): Coordinate? = suspendCoroutine {
val handlerThread = HandlerThread("getCurrentLocation() HandlerThread")
handlerThread.start()
try {
// Use of deprecated function is ok because we are pre android S
locationManager.requestSingleUpdate(
LocationManager.GPS_PROVIDER,
{ location ->
handlerThread.quit()
it.resume(
Coordinate(
location.latitude,
location.longitude
)
)
},
handlerThread.looper
)
}
catch (ex: Exception) {
ex.printStackTrace()
it.resumeWithException(ex)
}
}
As you can see, I use suspendCoroutine to make the asynchronous location call. This implementation works for some devices but I have problems on other devices. Sometimes the supending function never returns and waits forever because the location update callback is not called. The app also has the needed permissions and GPS is enabled.
What edge case leads to a state that the function never returns?
Logcat does not indicate any exception or other error. The app also does not crash. The only symptom is that the getCurrentLocationPreS() never returns.
Just because GPS is enabled doesn't mean that it is working properly. You might have a poor signal when being indoors or in areas packed with tall buildings. If you look at the implementation of requestSingleUpdate you will see it uses a timeout of 30s, so if the timeout expires, your callback will never be executed and your coroutine gets stuck indefinitely.
I would suggest to either to use a timeout for this call as well or consider using FusedLocationProviderClient which allows you to get the last known location in a safer way.
I would also suggest using Looper.getMainLooper(), the runtime overhead from temporarily switching to the main thread is negligible compared to the effort of making sure you are properly managing the HandlerThread
So my take on this would look something like this:
suspend fun getCurrentLocationPreS(): Coordinate? = withTimeoutOrNull(30.seconds){
suspendCoroutine { cont ->
try {
// Use of deprecated function is ok because we are pre android S
locationManager.requestSingleUpdate(
LocationManager.GPS_PROVIDER,
{ location ->
cont.resume(
Coordinate(
location.latitude,
location.longitude
)
)
},
Looper.getMainLooper()
)
}
catch (ex: Exception) {
ex.printStackTrace()
cont.resumeWithException(ex)
}
}
}
Related
I was having a problem implementing the Firebase anonymous sign-in function with Kotlin coroutine.
Following is the code for that:
Repository.kt
suspend fun getUserId(){
firebaseHelper.getUserId().collect{
if (it == "Successful"){
emit(it)
} else {
emit("Task unsuccessful")
}
}
}
FirebaseHelper.kt
fun getUserId() = flow {
val firebaseLoginAsync = Firebase.auth.signInAnonymously().await()
if (firebaseLoginAsync.user != null && !firebaseLoginAsync.user?.uid.isNullOrEmpty()) {
emit("Successful")
} else {
emit("Failed")
}
}
It works fine when the android device is connected to the internet.
But when I test this code without the internet it never completes, that is, the execution never reaches the if else block of FirebaseHelper.kt.
I was unable to find any resource that would help me understand the cause of this problem and any possible solution.
One idea that I can think of on the solution side is to forcefully cancel the await() functions execution after some time but I can't find anything related to implementation.
It works fine when the android device is connected to the internet.
Since an authentication operation requires an internet connection, then that's the expected behavior.
But when I test this code without the internet it never completes.
Without the internet, there is no way you can reach Firebase servers, hence that behavior. However, according to the official documentation of await() function:
This suspending function is cancellable. If the Job of the current coroutine is canceled or completed while this suspending function is waiting, this function immediately resumes with CancellationException.
Or you can simply check if the user is connected to the internet before performing the authentication.
The way that I made it work is with help of try catch block and withTimeout() function in FirebaseHelper.kt file. Following is the code of solution:
fun getUserID() = flow {
try {
val signInTask = Firebase.auth.signInAnonymously()
kotlinx.coroutines.withTimeout(5000) {
signInTask.await()
}
if (signInTask.isSuccessful){
emit("Successful")
} else {
emit("Failed")
}
} catch (e: Exception){
emit("Can't connect to the server\nPlease check your internet connection and retry")
}
}
withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T) runs the given suspend block for timeMillis milliseconds and throws TimeoutCancellationException if the timeout was exceeded.
I'm trying for the first time the coroutine function withTimeout. I'm trying to fetch the current location from the device GPS in Android, and add a timeout in case no location is available. I'm not controlling the process of fetching a location so I cannot make it cancellable easily.
Update: I ended up with a custom timeout logic as the android native api is not cancellable:
suspend fun LocationManager.listenLocationUpdate(): Location? =
withTimeoutOrNull(TIMEOUT) {
locationManager.listenLocationUpdate("gps")
}
private suspend fun LocationManager.listenLocationUpdate(provider: String) =
suspendCoroutine<Location?> { continuation ->
requestLocationUpdates(provider, 1000, 0f, object: TimeoutLocationListener{
override fun onLocationChanged(location: Location?) {
continuation.resume(location)
this#listenLocationUpdate.removeUpdates(this)
}
})
}
So the process of requesting a location belongs to the sdk and I cannot make it cancellale easily. Any suggestion?
For withTimeout[OrNull] to work, you need a cooperative cancellable coroutine. If the function you call is blocking, it will not work as expected. The calling coroutine will not even resume at all, let alone stop the processing of the blocking method. You can check this playground code to confirm this.
You have to have a cancellable API in the first place if you want to build coroutine-based APIs that are cancellable. It's hard to answer your question without knowing the exact function you're calling, though.
With Android's LocationManager, you can for instance wrap getCurrentLocation into a cancellable suspending function (this function is only available in API level 30+):
#RequiresApi(Build.VERSION_CODES.R)
#RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
suspend fun LocationManager.getCurrentLocation(provider: String, executor: Executor): Location? = suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
getCurrentLocation(provider, signal, executor) { location: Location? ->
cont.resume(location)
}
cont.invokeOnCancellation {
signal.cancel()
}
}
Otherwise you could also use callbackFlow to turn the listener-based API into a cancellable Flow-based API which unsubscribes upon cancellation (by removing the listener):
#OptIn(ExperimentalCoroutinesApi::class)
#RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
fun LocationManager.locationUpdates(provider: String, minTimeMs: Long, minDistance: Float = 0f): Flow<Location> =
callbackFlow {
val listener = LocationListener { location -> sendBlocking(location) }
requestLocationUpdates(provider, minTimeMs, minDistance, listener)
awaitClose {
removeUpdates(listener)
}
}
You can use first() on the returned flow if you just want one update, and this will automatically support cancellation:
suspend fun LocationManager.listenLocationUpdate(): Location? =
withTimeoutOrNull(TIMEOUT) {
locationManager.locationUpdates("gps", 1000).first()
}
If you use numUpdates = 1 in your location request, you should also be able to wrap the listener-based API into a single-shot suspending function too. Cancellation here could be done by just removing the listener.
I'm working in a project that need to show a NavigationView inside a fragment. It's working fine but consume a lot of battery (and depending of the device in some increase temperature considerably).
I checked the profiler in AS to try to identify the issue,
let's see...
As I can understand, Mapbox Navigation ask for location in High Accuracy every one second.
The question is, there is any way to configure the priority or the interval to reduce battery comsumption?
I followed the official docs to implement a custom LocationEngine, and works right for MapView, but not for NavigationView.
Anyone had this kind of issues in terms of performance with Mapbox Navigation? I've already test it in new and old devices and it's the same every time.
I'm using:
implementation "com.mapbox.navigation:ui:1.4.0"
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.6.0'
And here are some part of my implementation,
private fun initLocationEngine() {
locationEngine = LocationEngineProvider.getBestLocationEngine(requireContext())
// i don't know if is necessary to remove location updates before configure a new location engine
locationEngine?.removeLocationUpdates(this)
val request =
LocationEngineRequest.Builder(30000)
.setPriority(LocationEngineRequest.PRIORITY_BALANCED_POWER_ACCURACY)
.setMaxWaitTime(10000).build()
locationEngine?.requestLocationUpdates(request, this, getMainLooper())
}
override fun onMapReady(mapboxMap: MapboxMap) {
mapView = mapboxMap
mapView.setStyle(Style.TRAFFIC_DAY) {
initLocationEngine()
initCamera(mapboxMap)
}
}
private fun setupNavigationOptions(directionsRoute: DirectionsRoute): NavigationViewOptions {
val options = NavigationViewOptions.builder(requireContext())
options.directionsRoute(directionsRoute)
.navigationListener(this)
.feedbackListener(this)
.locationObserver(this)
.locationEngine(locationEngine)
return options.build()
}
private fun getNavigationRoute(origin: Point, destination: Point) {
val navigation = MapboxNavigation.defaultNavigationOptionsBuilder(getCurrentContext(), Mapbox.getAccessToken())
mapboxNavigation = MapboxNavigation(navigation.build())
val routeOptions = RouteOptions.builder()
.applyDefaultParams()
.accessToken(Mapbox.getAccessToken()!!)
.coordinates(coordinates)
.geometries(RouteUrl.GEOMETRY_POLYLINE6)
.profile(DirectionsCriteria.PROFILE_DRIVING)
.alternatives(false)
.voiceUnits(DirectionsCriteria.METRIC)
.build()
mapboxNavigation.requestRoutes(routeOptions, object : RoutesRequestCallback {
override fun onRoutesReady(routes: List<DirectionsRoute>) {
if (routes.isNotEmpty() && isAdded) {
val currentRoute = routes.first()
navigationView.startNavigation(setupNavigationOptions(currentRoute))
showNavigationMode()
}
}
override fun onRoutesRequestFailure(throwable: Throwable, routeOptions: RouteOptions) {
Timber.e("route request failure %s", throwable.toString())
}
override fun onRoutesRequestCanceled(routeOptions: RouteOptions) {
Timber.d("route request canceled")
}
})
}
// these methods are from LocationObserver callback
override fun onEnhancedLocationChanged(enhancedLocation: Location, keyPoints: List<Location>) {
// this method called every second, so, LocationEngine it's configured fine but the criteria and interval configuration does'nt work
}
override fun onRawLocationChanged(rawLocation: Location) {
}
// EDIT
After Yoshikage Ochi comment I made some changes to my setupNavigationOptions method:
private fun setupNavigationOptions(directionsRoute: DirectionsRoute): NavigationViewOptions {
val navViewOptions = NavigationViewOptions.builder(requireContext())
val navOpt = MapboxNavigation.defaultNavigationOptionsBuilder(requireContext(), Mapbox.getAccessToken())
val request =
LocationEngineRequest.Builder(30000)
.setPriority(LocationEngineRequest.PRIORITY_BALANCED_POWER_ACCURACY).build()
navOpt.locationEngineRequest(request)
navViewOptions.navigationOptions(navOpt.build())
navViewOptions.directionsRoute(directionsRoute)
.navigationListener(this)
.feedbackListener(this)
.locationObserver(this)
return options.build()
}
But unfortunatly it does'nt work. The period and the priority is the same (maybe the default), I've receiving updates every second and in HIGH_PRIORITY.
In your implementation, your setting will be overwritten by the default setting when the trip session starts as Google doc explains.
NavigationOptions has an option named locationEngineRequest that is used to configure LocationEngine (example).
Following code demonstrates how NavigationOptions can be used in NavigationViewOptions that is a parameter of NavigationView#startNavigation
val optionsBuilder = NavigationViewOptions.builder(this#MainActivity)
optionsBuilder.navigationOptions(MapboxNavigation
.defaultNavigationOptionsBuilder(this#MainActivity, Mapbox.getAccessToken())
.locationEngineRequest(LocationEngineRequest.Builder(5)
.build())
.build())
In the meantime, the location update energy consumption is way smaller than CPU energy consumption according to your performance result.
In this case, how about using FPS throttle mechanism that would reduce CPU power consumption? This setting clips the max FPS when the device is running on battery and meets some conditions. The default value is 20.
navigationView.retrieveNavigationMapboxMap()?.updateMapFpsThrottle(5);
I used this class to get current location of device for my map app. I'm using this with GooglePlayServices and its working fine, but I recently switched to HMS for Huawei devices if GooglePlayServices are not available on device. I replaced all GooglePlayServices classes with mirror objects from HMS imported lib and it compiled without errors. But as I call for current location, it will not return anything. No exception, no success or failure.
I did not receive callback to onLocationResult() or catch() block.
According to debugger last row called is val task = lp.requestLocationUpdates(lr, this, Looper.getMainLooper())
Anyone has this problem? This is clearly new issue. Testing this on Huawei P40 where GooglePlayServices are not available.
Also HuaweiMap is not working in release mode. getMapAsync() will not return onMapReady() callback. It got stuck there. But if I switch debug mode, it is working correctly.
UDPATE:
HuaweiMap is working now. Updated proguard. But Location is still not working. It is not working even in debug mode.
Code:
private inner class LocationCbHua(val lp: com.huawei.hms.location.FusedLocationProviderClient,
val onFailure: (()->Unit)? = null,
val onSuccess: (GpsLocation)->Unit)
: com.huawei.hms.location.LocationCallback() {
init {
val lr = com.huawei.hms.location.LocationRequest.create().apply {
priority = com.huawei.hms.location.LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 200
}
val lsr = com.huawei.hms.location.LocationSettingsRequest.Builder().run {
// setAlwaysShow(true) // TEST
addLocationRequest(lr)
build()
}
val check = com.huawei.hms.location.LocationServices.getSettingsClient(activity!!).checkLocationSettings(lsr)
check.addOnCompleteListener {
try {
check.getResultThrowException(com.huawei.hms.common.ApiException::class.java)
val task = lp.requestLocationUpdates(lr, this, Looper.getMainLooper())
task.addOnFailureListener {
onFailure?.invoke()
}
} catch (e: com.huawei.hms.common.ApiException) {
when (e.statusCode) {
com.huawei.hms.location.LocationSettingsStatusCodes.RESOLUTION_REQUIRED-> if(!locationResolutionAsked){
// Location settings are not satisfied. But could be fixed by showing the user a dialog.
try {
// Cast to a resolvable exception.
val re = e as com.huawei.hms.common.ResolvableApiException
// Show the dialog by calling startResolutionForResult(), and check the result in onActivityResult().
re.startResolutionForResult(mainActivity, MainActivity.REQUEST_LOCATION_SETTINGS)
locationResolutionAsked = true
} catch (e: Exception) {
e.printStackTrace()
}
}
com.huawei.hms.location.LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE->{
// Location settings are not satisfied. However, we have no way to fix the settings so we won't show the dialog.
App.warn("Location is not available")
onFailure?.invoke()
}
}
}
}
}
fun cancel(){
lp.removeLocationUpdates(this)
currLocCb = null
}
override fun onLocationResult(lr: com.huawei.hms.location.LocationResult) {
cancel()
val ll = lr.lastLocation
onSuccess(GpsLocation(ll.longitude, ll.latitude))
}
}
The possible cause is as follows:
After checkLocationSettings code was executed, an exception was catched during execution of code check.getResultThrowException. However, the catched error code is not 6 (RESOULTION_REQUIRED).
Therefore, code com.huawei.hms.location.LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE was directly executed to report Location is not available after code com.huawei.hms.location.LocationSettingsStatusCodes.RESOLUTION_REQUIRED-> if(!locationResolutionAsked) was executed.
As a result, neither exception nor location result was obtained. You are advised to add a code line at when (e.statusCode) to record error logs, and then continue error analysis.
Use OnSuccessListener instead of OnCompleteListener
val check = com.huawei.hms.location.LocationServices.getSettingsClient(activity!!).checkLocationSettings(lsr)
check.addOnSuccessListener{
lp.requestLocationUpdates(lr, this, Looper.getMainLooper())
}
You can also check this post:
https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0201272177441270079&fid=0101187876626530001
I am sending location data using co routines in workmanager.
I tried just using the workmanager but it does not do async work
I tried ListenableWorkmanager but that was too complicated for me so I am trying to use coroutines.
override fun doWork(): Result {
CoroutineScope(IO).launch {
val location = work()
fusedLocationClient!!.removeLocationUpdates(locationCallback)
string = logData(location)
}
return if(JSONObject(string).get("status") == "1"){
Result.success()
}else{
Result.retry()
}
}
I am having trouble on how to return the location from the work function
private suspend fun work():Location{
...............
fusedLocationClient!!.lastLocation.addOnSuccessListener { location ->
if (location != null) {
mCurrentLocation = location
// how do I send this location back to the fuction??
}
}.addOnFailureListener {
mLog.i(TAG, it.message)
}
return mCurrentLocation // if I do this could be a null right?
}
The Worker class only supports synchronous work. This means that when you return the result, the code in your worker needs to have completed its execution.
In your case you should use a ListenerWorker (or a CoroutineWorker if you prefer to use Kotlin).
Take a look at last year ADS talk on WorkManager, it covers this a bit. You can also view my talk Embracing WorkManager that covers the difference between the different Worker classes.