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:name="com.google.android.gms.version"
android:value="@integer/google_play_services_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> </application>
</manifest> </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:name=".locale.receiver.TaskerIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/> 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 -->
<uses-library <uses-library
android:name="com.google.android.maps" android:name="com.google.android.maps"

@ -2,6 +2,7 @@ package org.tasks.data
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.FilterListItem.NO_ORDER import com.todoroo.astrid.api.FilterListItem.NO_ORDER
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import org.tasks.filters.LocationFilters import org.tasks.filters.LocationFilters
@ -38,13 +39,13 @@ interface LocationDao {
+ " INNER JOIN tasks ON tasks._id = geofences.task" + " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :place AND arrival = 1 AND tasks.completed = 0" + " WHERE place = :place AND arrival = 1 AND tasks.completed = 0"
+ " AND tasks.deleted = 0 AND tasks.snoozeTime < :now AND tasks.hideUntil < :now") + " 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" @Query("SELECT geofences.* FROM geofences"
+ " INNER JOIN tasks ON tasks._id = geofences.task" + " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :place AND departure = 1 AND tasks.completed = 0" + " WHERE place = :place AND departure = 1 AND tasks.completed = 0"
+ " AND tasks.deleted = 0 AND tasks.snoozeTime < :now AND tasks.hideUntil < :now") + " 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" @Query("SELECT * FROM geofences"
+ " INNER JOIN places ON geofences.place = places.uid" + " 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 java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
import timber.log.Timber; import timber.log.Timber;
@ -36,10 +35,6 @@ public class Device {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE); return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE);
} }
public boolean supportsGeofences() {
return context.getResources().getBoolean(R.bool.support_geofences);
}
public boolean voiceInputAvailable() { public boolean voiceInputAvailable() {
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
List<ResolveInfo> activities = List<ResolveInfo> activities =

@ -112,8 +112,6 @@ class TaskDefaults : InjectingPreferenceFragment() {
updateRecurrence() updateRecurrence()
updateDefaultLocation() updateDefaultLocation()
updateTags() 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

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

Loading…
Cancel
Save