After I updated my phone to Android 12, my app (TargetSDKVersion 29) stopped to get location updates. So I have to update the app to API 31.
For this I want to write some instrumented tests for the location updates. I started with the simple test for requestLocationUpdates(). But in this simple test I don't get any location update.
Just in case I also adjusted the timestamps to 5s intervals to simulate some time between the updates - but no luck.
package de.leo.android.buddytracker_lib
import android.content.Context
import android.location.*
import android.os.Looper
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
private const val MOCK_PROVIDER = "MockLocationProvider"
private const val LOG_TAG ="**** Test ****"
class LocationHandlerAndroidTests : LocationListener {
private lateinit var context: Context
private lateinit var locationManager: LocationManager
private var locationUpdateCount = 0
private var currentLocation: Location? = null
override fun onLocationChanged(location: Location) {
locationUpdateCount++
currentLocation = location
}
#Before
fun init() {
context = InstrumentationRegistry.getInstrumentation().context
locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.addTestProvider(
MOCK_PROVIDER,
false,
false,
false,
false,
false,
false,
false,
Criteria.POWER_LOW,
Criteria.ACCURACY_FINE
)
locationManager.setTestProviderEnabled(MOCK_PROVIDER, true)
assertTrue(locationManager.isLocationEnabled)
locationUpdateCount = 0
currentLocation = null
Thread.sleep(5000L)
}
#After
fun tearDown() {
locationManager.removeTestProvider(MOCK_PROVIDER)
}
#Test
fun requestLocationUpdatesTest() {
Log.d(LOG_TAG, "Requesting Location Updates")
locationManager.requestLocationUpdates(MOCK_PROVIDER, 4000, 10.0f, this, Looper.getMainLooper())
val location1 = Location(MOCK_PROVIDER)
location1.latitude = 10.0
location1.longitude = 20.0
location1.accuracy = 2.0f
location1.time = 1000
location1.elapsedRealtimeNanos = 10000000000
Log.d(LOG_TAG, "set location 1")
locationManager.setTestProviderLocation(MOCK_PROVIDER, location1)
Thread.sleep(5000L)
val location2 = Location(MOCK_PROVIDER)
location2.latitude = 11.0
location2.longitude = 21.0
location2.accuracy = 2.0f
location2.time = 5000
location2.elapsedRealtimeNanos = 15000000000
Log.d(LOG_TAG, "set location 2")
locationManager.setTestProviderLocation(MOCK_PROVIDER, location2)
Thread.sleep(5000L)
// Check if your listener reacted the right way
assertEquals("Got location updates", 2, locationUpdateCount)
assertEquals(11.0, currentLocation?.latitude)
assertEquals(21.0, currentLocation?.longitude)
}
}
Any tips what I made wrong here?
Related
I'm making an application in Android (with Kotlin) that requires Google Maps services. When a button is pressed, it should locate me. However it is not accurate at all.
I use this version of Google Maps:
implementation 'com.google.android.gms:play-services-maps:18.0.2'
implementation 'com.google.android.gms:play-services-location:20.0.0'
Another versions:
Android Studio Chipmunk 2021.2.1 Patch 1
Tested with: Xiaomi Redmi Note 7 and Xiaomi Litte 9
And this is the code when I press the button:
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private lateinit var mMap: GoogleMap
private var coordinatesUbication: LatLng? = null
private fun getMyUbication() {
if (isGPSPermissions() && isGPSActive()) {
mMap.isMyLocationEnabled = true
mMap.uiSettings.isMyLocationButtonEnabled = false
// 1) Get latitude and longitude of the current position.
fusedLocationProviderClient.lastLocation.addOnCompleteListener { task ->
val location = task.result
if (location != null) {
// 2) Get coordinates.
val coordinates: LatLng? = LatLng(location.latitude, location.longitude)
this#MapsFragment.coordinatesUbication = coordinates
// 3) Set camera position.
CoroutineScope(Dispatchers.IO).launch {
cameraPosition(coordinatesUbication, Constants.CAMERA_ZOOM)
}
}
}
} else if (!isGPSPermissions()) {
myUbication = true
showDialogPermissions()
} else {
myUbication = true
}
}
I have finished the product I was developing, but currently, we track the users ( passenger ) location as well as the drivers too slowly.
This is the code I use to track and update the map with the passangers/ drivers icon as it moves :
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.google.android.gms.location.*
import mobi.audax.tupi.motorista.bin.task.GeoDecodeTask
import mobi.audax.tupi.passageiro.util.Prefs
class IntermitentLocationThread(val context: Context, val onLocationUpdate: (location: Location?) -> Unit) : LocationListener {
private var UPDATE_INTERVAL = (1000 * 10).toLong() // 10 segundos de intervalo
private val MAX_WAIT_TIME = UPDATE_INTERVAL * 2 // 20 segundos
private var bestLocation: Location? = null
fun requestLocation() {
this.locationService()
}
private fun locationService() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
val locationRequest = LocationRequest.create()
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locationRequest.fastestInterval = 1000
locationRequest.interval = UPDATE_INTERVAL
locationRequest.maxWaitTime = MAX_WAIT_TIME
locationRequest.smallestDisplacement = 15f
val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
fusedLocationProviderClient.lastLocation.addOnSuccessListener { location -> onLocationChanged(location) }
fusedLocationProviderClient.requestLocationUpdates(locationRequest, object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
onLocationChanged(locationResult.lastLocation)
}
}, Looper.myLooper()!!)
}
}
override fun onLocationChanged(location: Location) {
try {
Log.v("IntermitentLocationThread", "onLocationChanged")
if (location != null) {
val commons = LocationCommons()
// if (!commons.isMock(context, location) && commons.isBetterLocation(location, bestLocation)) {
Log.v("IntermitentLocationThread", "isBetter true")
val prefs = Prefs(context)
prefs.latitude = location.latitude.toFloat()
prefs.longitude = location.longitude.toFloat()
prefs.precisao = location.accuracy
prefs.velocidade = location.speed * 3.6f
prefs.bearing = location.bearing
if (location.extras.containsKey("satellites")) {
prefs.satellites = location.extras.getInt("satellites")
}
GeoDecodeTask(context, location).decoder { }
bestLocation = location
onLocationUpdate(bestLocation)
} else {
Log.v("IntermitentLocationThread", "isBetter false")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
and heres how i implement it in my Activity:
private void handleLocationUpdates() {
if (isLocationEnabled()) {
loadMapScene();
IntermitentLocationThread thread = new IntermitentLocationThread(this, location -> {
Log.e(TAG, "handleLocationUpdates: "+"pegado localização" );
clearPassageiroMapMarker();
addPassageiroMarker(new GeoCoordinates(location.getLatitude(),
location.getLongitude()), R.drawable.ic_passageiro);
passageiro.setLat(location.getLatitude());
passageiro.setLongitude(location.getLongitude());
SharedPreferences.Editor editor = this.getSharedPreferences(Constantss.PREFERENCES, MODE_PRIVATE).edit();
mapView.getCamera().lookAt(new GeoCoordinates(passageiro.getLat(), passageiro.getLongitude()));
return null;
});
thread.requestLocation();
} else {
Toast.makeText(this, "Por favor" + "ative sua localização...", Toast.LENGTH_LONG).show();
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
}
}
at the moment, I get passengers/drivers' locations in a very weird and odd consistency, it's not as " fluid " as uber does it. right now, my marker jumps from one point to another (because I clear the marker list and set another one in place, still working on having only one marker in a heremaps map) in a range of 5 to 15 seconds and everybody else on the internet seems to use this google engine to track one's location.
How is it possible to track users' location in a faster / smoother way?
i was able to find a better/fast tracking by using Google's Location Manager...
yes.. LocationManager.
The code i was using before that wasnt really accurate at all, with Googles Location Manager i was able to set a specific timer and keep it accurate.
More on : https://developer.android.com/reference/android/location/LocationManager
I am requesting location permissions and even after the user granted location permissions i cannot get gps location if location settings is disabled. How can i request user to enable the location settings in jetpack compose?
val multiplePermissionsState = rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_BACKGROUND_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION
)
)
Button(onClick = {
multiplePermissionsState.launchMultiplePermissionRequest()
},
modifier = Modifier.background(MaterialTheme.colors.primary)
)
Create enable location permission dialog like google map.
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.IntentSender
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.*
import com.google.android.gms.tasks.Task
#Composable
fun Layout() {
val context: Context = LocalContext.current
val settingResultRequest = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) { activityResult ->
if (activityResult.resultCode == RESULT_OK)
Log.d("appDebug", "Accepted")
else {
Log.d("appDebug", "Denied")
}
}
Button(onClick = {
checkLocationSetting(
context = context,
onDisabled = { intentSenderRequest ->
settingResultRequest.launch(intentSenderRequest)
},
onEnabled = { /* This will call when setting is already enabled */ }
)
}) {
Text(text = "Request permission")
}
}
// call this function on button click
fun checkLocationSetting(
context: Context,
onDisabled: (IntentSenderRequest) -> Unit,
onEnabled: () -> Unit
) {
val locationRequest = LocationRequest.create().apply {
interval = 1000
fastestInterval = 1000
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
val client: SettingsClient = LocationServices.getSettingsClient(context)
val builder: LocationSettingsRequest.Builder = LocationSettingsRequest
.Builder()
.addLocationRequest(locationRequest)
val gpsSettingTask: Task<LocationSettingsResponse> =
client.checkLocationSettings(builder.build())
gpsSettingTask.addOnSuccessListener { onEnabled() }
gpsSettingTask.addOnFailureListener { exception ->
if (exception is ResolvableApiException) {
try {
val intentSenderRequest = IntentSenderRequest
.Builder(exception.resolution)
.build()
onDisabled(intentSenderRequest)
} catch (sendEx: IntentSender.SendIntentException) {
// ignore here
}
}
}
}
You could use Google's Accompanist library:
(https://google.github.io/accompanist/permissions/)
Since their website contains example code, I'll not paste the code here.
I've written a unit test that use a mock location provider. The test passes when a location update is received, or fails when it times out.
The test passes fine running on emulated Pixel 3's, Android M through Android O. It times out on Android P and on Q. I also tested on a physical Pixel 3 with Q, still fails.
I've been beating my head against this for a while and can't figure out what's going on.
Test:
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.location.Location
import android.location.LocationManager
import androidx.test.rule.GrantPermissionRule
import design.inhale.androidtestutils.InstrumentedTest
import design.inhale.datasource.observer.NullDataSourceListener
import design.inhale.testutils.Latch
import design.inhale.utils.locationManager
import design.inhale.utils.mock
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.core.Is
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class DeviceLocationSourceTest: InstrumentedTest() {
private val mockProviderName = "MockProvider"
private val locationManager: LocationManager
get() = appContext.locationManager
#get:Rule
val permissionRule: GrantPermissionRule = grant(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
#Before
fun init() {
addMockLocationProvider()
}
#After
fun deinit() {
instrumentation.waitForIdleSync()
removeMockLocationProvider()
}
#Test(timeout = 10_000)
fun receiveLocationUpdate() {
val latch = Latch()
val listener = object: LocationSourceListener {
override fun onDataUpdated(data: Location) {
with(data) {
assertThat(latitude, Is(equalTo(0.0)))
assertThat(longitude, Is(equalTo(0.0)))
}
latch.release()
}
}
mockLocationSource(listener).start()
instrumentation.waitForIdleSync() // in case we're hitting race conditions?
updateMockLocation(0.0, 0.0)
latch.await()
}
#Suppress("SameParameterValue")
private fun updateMockLocation(latitude: Double, longitude: Double) {
val location = Location(mockProviderName).mock(latitude, longitude)
locationManager.setTestProviderLocation(mockProviderName, location)
}
private fun mockLocationSource(listener: LocationSourceListener = NullDataSourceListener()) =
DeviceLocationSource(appContext, mockProviderName, listener)
private fun addMockLocationProvider() {
with(locationManager) {
try {
addTestProvider(
mockProviderName,
false,
false,
false,
false,
true,
true,
true,
0,
5)
} catch (e: IllegalArgumentException) {
// If this is caught, the mock provider already exists
}
setTestProviderEnabled(mockProviderName, true)
}
}
private fun removeMockLocationProvider() = locationManager.removeTestProvider(mockProviderName)
}
Manifest:
<manifest package="design.inhale.locationapi"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
androidTest/Manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="design.inhale.locationapi"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
</manifest>
Location.mock():
#SuppressLint("ObsoleteSdkInt")
fun Location.mock(latitude: Double = 0.0, longitude: Double = 0.0, accuracy: Float = 0f): Location {
this.latitude = latitude
this.longitude = longitude
this.accuracy = accuracy
this.time = currentTimeMillis()
if (SDK_INT > JELLY_BEAN) this.elapsedRealtimeNanos = elapsedRealtimeNanos()
return this
}
Turns out I needed to add permission for background location access (I guess the test counts as running in the background?).
Added permission to the test manifest.
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>`
and changed the test to grant it:
val permissionRule: GrantPermissionRule = grant(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION)
Note that you'll want to make sure you don't add ACCESS_BACKGROUND_LOCATION to your production manifest. Google will pull your app if they find you're using this permission without a good reason.
Hello I'm using RxAndroidBLE to detect a BLE device. On android 6 >= everything seems to work okay but not on a 4.3 device.
My app can only discover the desirable BLE device only once at start. After the device has been discovered no more new discoveries at all until I restart the app. Any advice would be highly appreciated.
Below minimum (not)working code example:
MainActivity
import android.content.Context
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import com.polidea.rxandroidble.RxBleClient
import com.polidea.rxandroidble.exceptions.BleScanException
import com.polidea.rxandroidble.scan.ScanResult
import com.polidea.rxandroidble.scan.ScanSettings
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startLeScan(applicationContext)
}
private var rxBleClient: RxBleClient? = null
private var scanSubscription: Subscription? = null
private var handler: Handler? = null
private var timer: Timer? = null
private var timerTask: TimerTask? = null
private var delay: Int = 0
private fun isScanning(): Boolean {
return scanSubscription != null
}
fun startLeScan(context: Context) {
rxBleClient = MyaPP.getRxBleClient(context)
if (isScanning()) {
scanSubscription?.unsubscribe()
} else {
scanSubscription = rxBleClient?.scanBleDevices(
com.polidea.rxandroidble.scan.ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build())
?.observeOn(AndroidSchedulers.mainThread())
//?.doOnNext(this::newDevicesFound)
?.doOnUnsubscribe(this::clearSubscription)
?.subscribe(this::newDevicesFound, this::onScanFailure)
}
if(handler == null) {
handler = Handler()
timer = Timer(false)
timerTask = object : TimerTask() {
override fun run() {
handler?.post {
if (delay > 7) {
delay = 0
val service = Executors.newSingleThreadExecutor()
service.submit(Runnable {
//startLeScan(context)
})
} else {
delay = delay + 1
}
}
}
}
timer?.scheduleAtFixedRate(timerTask, 0, 300)
}
}
private fun newDevicesFound(devices: ScanResult) {
Log.d("WHYY??", devices.bleDevice.name)
}
fun stopScan() {
scanSubscription?.unsubscribe()
destroy()
}
private fun clearSubscription() {
scanSubscription = null
}
private fun onScanFailure(throwable: Throwable) {
if (throwable is BleScanException) {
handleBleScanException(throwable)
}
}
private fun handleBleScanException(bleScanException: BleScanException) {
val text: String
when (bleScanException.reason) {
BleScanException.BLUETOOTH_NOT_AVAILABLE -> text = "Bluetooth is not available"
BleScanException.BLUETOOTH_DISABLED -> text = "Enable bluetooth and try again"
BleScanException.LOCATION_PERMISSION_MISSING -> text = "On Android 6.0 location permission is required. Implement Runtime Permissions"
BleScanException.LOCATION_SERVICES_DISABLED -> text = "Location services needs to be enabled on Android 6.0"
BleScanException.SCAN_FAILED_ALREADY_STARTED -> text = "Scan with the same filters is already started"
BleScanException.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> text = "Failed to register application for bluetooth scan"
BleScanException.SCAN_FAILED_FEATURE_UNSUPPORTED -> text = "Scan with specified parameters is not supported"
BleScanException.SCAN_FAILED_INTERNAL_ERROR -> text = "Scan failed due to internal error"
BleScanException.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES -> text = "Scan cannot start due to limited hardware resources"
BleScanException.UNDOCUMENTED_SCAN_THROTTLE -> text = String.format(
Locale.getDefault(),
"Android 7+ does not allow more scans. Try in %d seconds",
secondsTill(bleScanException.retryDateSuggestion)
)
BleScanException.UNKNOWN_ERROR_CODE, BleScanException.BLUETOOTH_CANNOT_START -> text = "Unable to start scanning"
else -> text = "Unable to start scanning"
}
Log.w("EXCEPTION", text, bleScanException)
}
private fun secondsTill(retryDateSuggestion: Date?): Long {
if (retryDateSuggestion != null) {
return TimeUnit.MILLISECONDS.toSeconds(retryDateSuggestion.time - System.currentTimeMillis())
}
return 0
}
private fun destroy() {
timer?.cancel()
handler?.removeCallbacks(timerTask)
handler = null
timerTask = null
timer = null
}
}
MyaPP
import android.app.Application
import android.content.Context
import com.polidea.rxandroidble.RxBleClient
import com.polidea.rxandroidble.internal.RxBleLog
class MyaPP: Application() {
private var rxBleClient: RxBleClient? = null
companion object {
fun getRxBleClient(context: Context): RxBleClient? {
val application = context.applicationContext as MyaPP
return application.rxBleClient
}
}
override fun onCreate() {
super.onCreate()
rxBleClient = RxBleClient.create(this)
RxBleClient.setLogLevel(RxBleLog.DEBUG)
}
}
build.gradle
compile "com.polidea.rxandroidble:rxandroidble:1.5.0"
implementation 'io.reactivex:rxandroid:1.2.1'
manifest
<application
android:name=".MyaPP"
Your code looks a lot like the library's sample app (version 1.5.0, branch master-rxjava1). I have checked that recently on Android 4.4.4 which is the oldest I have and it worked fine. There were no API changes between 4.3 and 4.4.
What you may be experiencing is a behaviour specific to your device (feel free to share your phone model) in which it only callbacks for the first time it scans a particular peripheral. There are some threads about this topic already like this one.