Fix location Spam (#863)

* Reduce/eliminate the number of duplicate location updates.

* Limit accurate location requests to once per minute max.

* Remove some validation code.

* Swap around statements.

* Send location every 15 minutes even if it's the same.
This commit is contained in:
Justin Bassett 2020-09-01 10:05:30 -04:00 committed by GitHub
parent 44551543bd
commit 72888cc754
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -20,6 +20,7 @@ import com.google.android.gms.location.LocationServices
import io.homeassistant.companion.android.R import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.database.AppDatabase import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Attribute
import io.homeassistant.companion.android.domain.integration.IntegrationUseCase import io.homeassistant.companion.android.domain.integration.IntegrationUseCase
import io.homeassistant.companion.android.domain.integration.UpdateLocation import io.homeassistant.companion.android.domain.integration.UpdateLocation
import javax.inject.Inject import javax.inject.Inject
@ -62,23 +63,29 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO) private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
lateinit var latestContext: Context
private var isBackgroundLocationSetup = false
private var isZoneLocationSetup = false
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
ensureInjected(context) latestContext = context
ensureInjected()
when (intent.action) { when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_BOOT_COMPLETED,
ACTION_REQUEST_LOCATION_UPDATES -> setupLocationTracking(context) ACTION_REQUEST_LOCATION_UPDATES -> setupLocationTracking()
ACTION_PROCESS_LOCATION -> handleLocationUpdate(intent) ACTION_PROCESS_LOCATION -> handleLocationUpdate(intent)
ACTION_PROCESS_GEO -> handleGeoUpdate(context, intent) ACTION_PROCESS_GEO -> handleGeoUpdate(intent)
ACTION_REQUEST_ACCURATE_LOCATION_UPDATE -> requestSingleAccurateLocation(context) ACTION_REQUEST_ACCURATE_LOCATION_UPDATE -> requestSingleAccurateLocation()
else -> Log.w(TAG, "Unknown intent action: ${intent.action}!") else -> Log.w(TAG, "Unknown intent action: ${intent.action}!")
} }
} }
private fun ensureInjected(context: Context) { private fun ensureInjected() {
if (context.applicationContext is GraphComponentAccessor) { if (latestContext.applicationContext is GraphComponentAccessor) {
DaggerSensorComponent.builder() DaggerSensorComponent.builder()
.appComponent((context.applicationContext as GraphComponentAccessor).appComponent) .appComponent((latestContext.applicationContext as GraphComponentAccessor).appComponent)
.build() .build()
.inject(this) .inject(this)
} else { } else {
@ -86,49 +93,57 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
} }
} }
private fun setupLocationTracking(context: Context) { private fun setupLocationTracking() {
if (!checkPermission(context)) { if (!checkPermission(latestContext)) {
Log.w(TAG, "Not starting location reporting because of permissions.") Log.w(TAG, "Not starting location reporting because of permissions.")
return return
} }
val sensorDao = AppDatabase.getInstance(context).sensorDao() val backgroundEnabled = isEnabled(latestContext, backgroundLocation.id)
val zoneEnabled = isEnabled(latestContext, zoneLocation.id)
ioScope.launch { ioScope.launch {
try { try {
removeAllLocationUpdateRequests(context) if (!backgroundEnabled && !zoneEnabled) {
removeAllLocationUpdateRequests()
if (sensorDao.get(backgroundLocation.id)?.enabled == true) isBackgroundLocationSetup = false
requestLocationUpdates(context) isZoneLocationSetup = false
if (sensorDao.get(zoneLocation.id)?.enabled == true) }
requestZoneUpdates(context) if (backgroundEnabled && !isBackgroundLocationSetup) {
isBackgroundLocationSetup = true
requestLocationUpdates()
}
if (zoneEnabled && !isZoneLocationSetup) {
isZoneLocationSetup = true
requestZoneUpdates()
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Issue setting up location tracking", e) Log.e(TAG, "Issue setting up location tracking", e)
} }
} }
} }
private fun removeAllLocationUpdateRequests(context: Context) { private fun removeAllLocationUpdateRequests() {
Log.d(TAG, "Removing all location requests.") Log.d(TAG, "Removing all location requests.")
val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(latestContext)
val backgroundIntent = getLocationUpdateIntent(context, false) val backgroundIntent = getLocationUpdateIntent(false)
fusedLocationProviderClient.removeLocationUpdates(backgroundIntent) fusedLocationProviderClient.removeLocationUpdates(backgroundIntent)
val geofencingClient = LocationServices.getGeofencingClient(context) val geofencingClient = LocationServices.getGeofencingClient(latestContext)
val zoneIntent = getLocationUpdateIntent(context, true) val zoneIntent = getLocationUpdateIntent(true)
geofencingClient.removeGeofences(zoneIntent) geofencingClient.removeGeofences(zoneIntent)
} }
private fun requestLocationUpdates(context: Context) { private fun requestLocationUpdates() {
if (!checkPermission(context)) { if (!checkPermission(latestContext)) {
Log.w(TAG, "Not registering for location updates because of permissions.") Log.w(TAG, "Not registering for location updates because of permissions.")
return return
} }
Log.d(TAG, "Registering for location updates.") Log.d(TAG, "Registering for location updates.")
val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(latestContext)
val intent = getLocationUpdateIntent(context, false) val intent = getLocationUpdateIntent(false)
fusedLocationProviderClient.requestLocationUpdates( fusedLocationProviderClient.requestLocationUpdates(
createLocationRequest(), createLocationRequest(),
@ -136,8 +151,8 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
) )
} }
private suspend fun requestZoneUpdates(context: Context) { private suspend fun requestZoneUpdates() {
if (!checkPermission(context)) { if (!checkPermission(latestContext)) {
Log.w(TAG, "Not registering for zone based updates because of permissions.") Log.w(TAG, "Not registering for zone based updates because of permissions.")
return return
} }
@ -145,8 +160,8 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
Log.d(TAG, "Registering for zone based location updates") Log.d(TAG, "Registering for zone based location updates")
try { try {
val geofencingClient = LocationServices.getGeofencingClient(context) val geofencingClient = LocationServices.getGeofencingClient(latestContext)
val intent = getLocationUpdateIntent(context, true) val intent = getLocationUpdateIntent(true)
val geofencingRequest = createGeofencingRequest() val geofencingRequest = createGeofencingRequest()
geofencingClient.addGeofences( geofencingClient.addGeofences(
geofencingRequest, geofencingRequest,
@ -168,7 +183,7 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
} }
} }
private fun handleGeoUpdate(context: Context, intent: Intent) { private fun handleGeoUpdate(intent: Intent) {
Log.d(TAG, "Received geofence update.") Log.d(TAG, "Received geofence update.")
val geofencingEvent = GeofencingEvent.fromIntent(intent) val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) { if (geofencingEvent.hasError()) {
@ -181,7 +196,7 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
TAG, TAG,
"Geofence location accuracy didn't meet requirements, requesting new location." "Geofence location accuracy didn't meet requirements, requesting new location."
) )
requestSingleAccurateLocation(context) requestSingleAccurateLocation()
} else { } else {
sendLocationUpdate(geofencingEvent.triggeringLocation) sendLocationUpdate(geofencingEvent.triggeringLocation)
} }
@ -208,19 +223,43 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
if (Build.VERSION.SDK_INT >= 26) location.verticalAccuracyMeters.toInt() else 0 if (Build.VERSION.SDK_INT >= 26) location.verticalAccuracyMeters.toInt() else 0
) )
val sensorDao = AppDatabase.getInstance(latestContext).sensorDao()
val fullSensor = sensorDao.getFull(backgroundLocation.id)
val locationEntity = fullSensor?.sensor
val lastLocationSend = fullSensor?.attributes?.firstOrNull { it.name == "lastLocationSent" }?.value?.toLongOrNull() ?: 0L
val now = System.currentTimeMillis()
if (locationEntity?.state == updateLocation.gps.contentToString()) {
if (now >= lastLocationSend + 900000) {
Log.d(TAG, "Sending location since it's been more than 15 minutes")
} else {
Log.d(TAG, "Same location as last update, not sending to HA")
return
}
}
ioScope.launch { ioScope.launch {
try { try {
integrationUseCase.updateLocation(updateLocation) integrationUseCase.updateLocation(updateLocation)
onSensorUpdated(
latestContext,
backgroundLocation,
updateLocation.gps.contentToString(),
"",
mapOf(
"lastLocationSent" to now.toString()
)
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Could not update location.", e) Log.e(TAG, "Could not update location.", e)
} }
} }
} }
private fun getLocationUpdateIntent(context: Context, isGeofence: Boolean): PendingIntent { private fun getLocationUpdateIntent(isGeofence: Boolean): PendingIntent {
val intent = Intent(context, LocationSensorManager::class.java) val intent = Intent(latestContext, LocationSensorManager::class.java)
intent.action = if (isGeofence) ACTION_PROCESS_GEO else ACTION_PROCESS_LOCATION intent.action = if (isGeofence) ACTION_PROCESS_GEO else ACTION_PROCESS_LOCATION
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getBroadcast(latestContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
private fun createLocationRequest(): LocationRequest { private fun createLocationRequest(): LocationRequest {
@ -237,6 +276,7 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
private suspend fun createGeofencingRequest(): GeofencingRequest { private suspend fun createGeofencingRequest(): GeofencingRequest {
val geofencingRequestBuilder = GeofencingRequest.Builder() val geofencingRequestBuilder = GeofencingRequest.Builder()
// TODO cache the zones on device so we don't need to reach out each time
integrationUseCase.getZones().forEach { integrationUseCase.getZones().forEach {
geofencingRequestBuilder.addGeofence( geofencingRequestBuilder.addGeofence(
Geofence.Builder() Geofence.Builder()
@ -254,21 +294,32 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
return geofencingRequestBuilder.build() return geofencingRequestBuilder.build()
} }
private fun requestSingleAccurateLocation(context: Context) { private fun requestSingleAccurateLocation() {
if (!checkPermission(context)) { if (!checkPermission(latestContext)) {
Log.w(TAG, "Not getting single accurate location because of permissions.") Log.w(TAG, "Not getting single accurate location because of permissions.")
return return
} }
val now = System.currentTimeMillis()
val sensorDao = AppDatabase.getInstance(latestContext).sensorDao()
val fullSensor = sensorDao.getFull(backgroundLocation.id)
val latestAccurateLocation = fullSensor?.attributes?.firstOrNull { it.name == "lastAccurateLocationRequest" }?.value?.toLongOrNull() ?: 0L
// Only update accurate location at most once a minute
if (now < latestAccurateLocation + 60000) {
Log.d(TAG, "Not requesting accurate location, last accurate location was too recent")
return
}
sensorDao.add(Attribute(backgroundLocation.id, "lastAccurateLocationRequest", now.toString()))
val maxRetries = 5 val maxRetries = 5
val request = createLocationRequest() val request = createLocationRequest()
request.priority = LocationRequest.PRIORITY_HIGH_ACCURACY request.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
request.numUpdates = maxRetries request.numUpdates = maxRetries
LocationServices.getFusedLocationProviderClient(context) LocationServices.getFusedLocationProviderClient(latestContext)
.requestLocationUpdates( .requestLocationUpdates(
request, request,
object : LocationCallback() { object : LocationCallback() {
val wakeLock: PowerManager.WakeLock? = val wakeLock: PowerManager.WakeLock? =
getSystemService(context, PowerManager::class.java) getSystemService(latestContext, PowerManager::class.java)
?.newWakeLock( ?.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, PowerManager.PARTIAL_WAKE_LOCK,
"HomeAssistant::AccurateLocation" "HomeAssistant::AccurateLocation"
@ -333,9 +384,10 @@ class LocationSensorManager : BroadcastReceiver(), SensorManager {
override fun requestSensorUpdate( override fun requestSensorUpdate(
context: Context context: Context
) { ) {
ensureInjected(context) latestContext = context
ensureInjected()
if (isEnabled(context, zoneLocation.id) || isEnabled(context, backgroundLocation.id)) if (isEnabled(context, zoneLocation.id) || isEnabled(context, backgroundLocation.id))
setupLocationTracking(context) setupLocationTracking()
if (isEnabled(context, backgroundLocation.id)) { if (isEnabled(context, backgroundLocation.id)) {
context.sendBroadcast( context.sendBroadcast(
Intent(context, this.javaClass).apply { Intent(context, this.javaClass).apply {