Add LocationManager proximity alerts

pull/1360/head
Alex Baker 5 years ago
parent 598cbba8c1
commit 99dee06c64

@ -1,12 +0,0 @@
package org.tasks.location
import org.tasks.data.Place
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class GeofenceApi @Inject constructor() {
fun registerAll() {}
fun update(place: Place?) {}
fun update(place: String?) {}
fun update(taskId: Long) {}
}

@ -0,0 +1,41 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.location.LocationManager
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
import javax.inject.Inject
class GeofenceClient @Inject constructor(@ApplicationContext private val context: Context) {
private val client = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@SuppressLint("MissingPermission")
fun addGeofences(@Suppress("UNUSED_PARAMETER") geofence: MergedGeofence) {
client.addProximityAlert(
geofence.latitude,
geofence.longitude,
geofence.radius.toFloat(),
-1,
createPendingIntent(geofence.place.id)
)
}
fun removeGeofences(@Suppress("UNUSED_PARAMETER") place: Place) {
client.removeProximityAlert(createPendingIntent(place.id))
}
private fun createPendingIntent(place: Long) =
PendingIntent.getBroadcast(
context,
0,
Intent(context, GeofenceTransitionsIntentService.Broadcast::class.java)
.setData(Uri.parse("tasks://geofence/$place")),
PendingIntent.FLAG_UPDATE_CURRENT
)
}

@ -0,0 +1,44 @@
package org.tasks.location
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.location.LocationManager
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.Notifier
import org.tasks.data.LocationDao
import org.tasks.injection.InjectingJobIntentService
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class GeofenceTransitionsIntentService : InjectingJobIntentService() {
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var notifier: Notifier
override suspend fun doWork(intent: Intent) {
val arrival = intent.getBooleanExtra(LocationManager.KEY_PROXIMITY_ENTERING, false)
Timber.d("geofence[${intent.data}] arrival[$arrival]")
val place = intent.data?.lastPathSegment?.toLongOrNull()?.let { locationDao.getPlace(it) }
if (place == null) {
Timber.e("Failed to find place ${intent.data}")
return
}
val geofences = if (arrival) {
locationDao.getArrivalGeofences(place.uid!!)
} else {
locationDao.getDepartureGeofences(place.uid!!)
}
notifier.triggerNotifications(place.id, geofences, arrival)
}
class Broadcast : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
enqueueWork(
context,
GeofenceTransitionsIntentService::class.java,
JOB_ID_GEOFENCE_TRANSITION,
intent)
}
}
}

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="support_geofences">false</bool>
</resources>

@ -13,12 +13,6 @@
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<receiver android:name=".location.GeofenceTransitionsIntentService$Broadcast"/>
<service
android:exported="false"
android:name=".location.GeofenceTransitionsIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
</application>
</manifest>

@ -1,66 +0,0 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofencingRequest
import com.google.android.gms.location.LocationServices
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.data.LocationDao
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
import org.tasks.preferences.PermissionChecker
import timber.log.Timber
import javax.inject.Inject
class GeofenceApi @Inject constructor(
@param:ApplicationContext private val context: Context,
private val permissionChecker: PermissionChecker,
private val locationDao: LocationDao) {
suspend fun registerAll() = locationDao.getPlacesWithGeofences().forEach { update(it) }
suspend fun update(taskId: Long) = update(locationDao.getPlaceForTask(taskId))
suspend fun update(place: String) = update(locationDao.getPlace(place))
@SuppressLint("MissingPermission")
suspend fun update(place: Place?) {
if (place == null || !permissionChecker.canAccessBackgroundLocation()) {
return
}
val client = LocationServices.getGeofencingClient(context)
val geofence = locationDao.getGeofencesByPlace(place.uid!!)
if (geofence != null) {
Timber.d("Adding geofence for %s", geofence)
client.addGeofences(
GeofencingRequest.Builder().addGeofence(toGoogleGeofence(geofence)).build(),
PendingIntent.getBroadcast(
context,
0,
Intent(context, GeofenceTransitionsIntentService.Broadcast::class.java),
PendingIntent.FLAG_UPDATE_CURRENT))
} else {
Timber.d("Removing geofence for %s", place)
client.removeGeofences(listOf(place.id.toString()))
}
}
private fun toGoogleGeofence(geofence: MergedGeofence): Geofence {
var transitionTypes = 0
if (geofence.arrival) {
transitionTypes = transitionTypes or GeofencingRequest.INITIAL_TRIGGER_ENTER
}
if (geofence.departure) {
transitionTypes = transitionTypes or GeofencingRequest.INITIAL_TRIGGER_EXIT
}
return Geofence.Builder()
.setCircularRegion(geofence.latitude, geofence.longitude, geofence.radius.toFloat())
.setRequestId(geofence.uid)
.setTransitionTypes(transitionTypes)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.build()
}
}

@ -0,0 +1,48 @@
package org.tasks.location
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofencingRequest
import com.google.android.gms.location.LocationServices
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.data.MergedGeofence
import org.tasks.data.Place
import javax.inject.Inject
class GeofenceClient @Inject constructor(@ApplicationContext private val context: Context) {
private val client = LocationServices.getGeofencingClient(context)
@SuppressLint("MissingPermission")
fun addGeofences(geofence: MergedGeofence) {
client.addGeofences(
GeofencingRequest.Builder().addGeofence(toGoogleGeofence(geofence)).build(),
PendingIntent.getBroadcast(
context,
0,
Intent(context, GeofenceTransitionsIntentService.Broadcast::class.java),
PendingIntent.FLAG_UPDATE_CURRENT))
}
fun removeGeofences(place: Place) {
client.removeGeofences(listOf(place.id.toString()))
}
private fun toGoogleGeofence(geofence: MergedGeofence): Geofence {
var transitionTypes = 0
if (geofence.arrival) {
transitionTypes = transitionTypes or GeofencingRequest.INITIAL_TRIGGER_ENTER
}
if (geofence.departure) {
transitionTypes = transitionTypes or GeofencingRequest.INITIAL_TRIGGER_EXIT
}
return Geofence.Builder()
.setCircularRegion(geofence.latitude, geofence.longitude, geofence.radius.toFloat())
.setRequestId(geofence.uid)
.setTransitionTypes(transitionTypes)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.build()
}
}

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="support_geofences">true</bool>
</resources>

@ -493,6 +493,12 @@
android:name=".locale.receiver.TaskerIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<receiver android:name=".location.GeofenceTransitionsIntentService$Broadcast"/>
<service
android:exported="false"
android:name=".location.GeofenceTransitionsIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<!-- Uses Library -->
<uses-library
android:name="com.google.android.maps"

@ -2,6 +2,7 @@ package org.tasks.data
import androidx.lifecycle.LiveData
import androidx.room.*
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.FilterListItem.NO_ORDER
import com.todoroo.astrid.data.Task
import org.tasks.filters.LocationFilters
@ -38,13 +39,13 @@ interface LocationDao {
+ " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :place AND arrival = 1 AND tasks.completed = 0"
+ " AND tasks.deleted = 0 AND tasks.snoozeTime < :now AND tasks.hideUntil < :now")
suspend fun getArrivalGeofences(place: String, now: Long): List<Geofence>
suspend fun getArrivalGeofences(place: String, now: Long = now()): List<Geofence>
@Query("SELECT geofences.* FROM geofences"
+ " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :place AND departure = 1 AND tasks.completed = 0"
+ " AND tasks.deleted = 0 AND tasks.snoozeTime < :now AND tasks.hideUntil < :now")
suspend fun getDepartureGeofences(place: String, now: Long): List<Geofence>
suspend fun getDepartureGeofences(place: String, now: Long = now()): List<Geofence>
@Query("SELECT * FROM geofences"
+ " INNER JOIN places ON geofences.place = places.uid"

@ -0,0 +1,34 @@
package org.tasks.location
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.preferences.PermissionChecker
import timber.log.Timber
import javax.inject.Inject
class GeofenceApi @Inject constructor(
private val permissionChecker: PermissionChecker,
private val locationDao: LocationDao,
private val client: GeofenceClient
) {
suspend fun registerAll() = locationDao.getPlacesWithGeofences().forEach { update(it) }
suspend fun update(taskId: Long) = update(locationDao.getPlaceForTask(taskId))
suspend fun update(place: String) = update(locationDao.getPlace(place))
suspend fun update(place: Place?) {
if (place == null || !permissionChecker.canAccessBackgroundLocation()) {
return
}
locationDao
.getGeofencesByPlace(place.uid!!)?.let {
Timber.d("Adding geofence for %s", it)
client.addGeofences(it)
}
?: place.let {
Timber.d("Removing geofence for %s", it)
client.removeGeofences(it)
}
}
}

@ -13,7 +13,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.locale.Locale;
import timber.log.Timber;
@ -36,10 +35,6 @@ public class Device {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE);
}
public boolean supportsGeofences() {
return context.getResources().getBoolean(R.bool.support_geofences);
}
public boolean voiceInputAvailable() {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> activities =

@ -112,8 +112,6 @@ class TaskDefaults : InjectingPreferenceFragment() {
updateRecurrence()
updateDefaultLocation()
updateTags()
requires(device.supportsGeofences(), R.string.p_default_location_reminder_key, R.string.p_default_location_radius)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

@ -65,7 +65,7 @@ class LocationControlSet : TaskEditControlFragment() {
geofenceOptions.visibility = View.GONE
locationAddress.visibility = View.GONE
} else {
geofenceOptions.visibility = if (device.supportsGeofences()) View.VISIBLE else View.GONE
geofenceOptions.visibility = View.VISIBLE
geofenceOptions.setImageResource(
if (permissionChecker.canAccessBackgroundLocation()
&& (location.isArrival || location.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px)

Loading…
Cancel
Save