mirror of https://github.com/tasks/tasks
Initial support for geofence reminders
* Geofences are not cleaned up when tasks are deleted/completed * Does not gracefully handle any error conditionspull/281/head
parent
11691f37bb
commit
a9723e025b
@ -0,0 +1,6 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.tasks">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.tasks.dialogs;
|
||||||
|
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
|
||||||
|
import org.tasks.location.LocationApi;
|
||||||
|
import org.tasks.location.OnLocationPickedHandler;
|
||||||
|
|
||||||
|
public class LocationPickerDialog {
|
||||||
|
public static void pickLocation(LocationApi locationApi, Fragment fragment, OnLocationPickedHandler onLocationPickedHandler) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class GeofenceApi {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GeofenceApi() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(List<Geofence> activeGeofences) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel(Geofence geofence) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
public class GeofenceTransitionsIntentService {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class LocationApi {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LocationApi() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package org.tasks.dialogs;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
|
import com.google.android.gms.location.places.Place;
|
||||||
|
import com.google.android.gms.location.places.PlaceBuffer;
|
||||||
|
import com.google.android.gms.maps.model.LatLng;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.tasks.R;
|
||||||
|
import org.tasks.location.Geofence;
|
||||||
|
import org.tasks.location.LocationApi;
|
||||||
|
import org.tasks.location.OnLocationPickedHandler;
|
||||||
|
import org.tasks.location.PlaceAutocompleteAdapter;
|
||||||
|
|
||||||
|
public class LocationPickerDialog extends DialogFragment {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LocationPickerDialog.class);
|
||||||
|
|
||||||
|
private static final String FRAG_TAG_LOCATION_PICKER = "frag_tag_location_picker";
|
||||||
|
private LocationApi locationApi;
|
||||||
|
private FragmentActivity fragmentActivity;
|
||||||
|
private OnLocationPickedHandler onLocationPickedHandler;
|
||||||
|
private PlaceAutocompleteAdapter mAdapter;
|
||||||
|
|
||||||
|
public static void pickLocation(LocationApi locationApi, Fragment fragment, OnLocationPickedHandler onLocationPickedHandler) {
|
||||||
|
LocationPickerDialog locationPickerDialog = new LocationPickerDialog();
|
||||||
|
locationPickerDialog.initialize(locationApi, fragment.getActivity(), onLocationPickedHandler);
|
||||||
|
locationPickerDialog.show(fragment.getChildFragmentManager(), FRAG_TAG_LOCATION_PICKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View layout = inflater.inflate(R.layout.location_picker_dialog, null);
|
||||||
|
EditText addressEntry = (EditText) layout.findViewById(R.id.address_entry);
|
||||||
|
|
||||||
|
addressEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||||
|
CharSequence search = v.getText();
|
||||||
|
mAdapter.getAutocomplete(search);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mAdapter = new PlaceAutocompleteAdapter(locationApi, fragmentActivity, android.R.layout.simple_list_item_1);
|
||||||
|
ListView list = (ListView) layout.findViewById(R.id.list);
|
||||||
|
list.setAdapter(mAdapter);
|
||||||
|
list.setOnItemClickListener(mAutocompleteClickListener);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize(LocationApi locationApi, FragmentActivity fragmentActivity, OnLocationPickedHandler onLocationPickedHandler) {
|
||||||
|
this.locationApi = locationApi;
|
||||||
|
this.fragmentActivity = fragmentActivity;
|
||||||
|
this.onLocationPickedHandler = onLocationPickedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void error(String text) {
|
||||||
|
log.error(text);
|
||||||
|
Toast.makeText(fragmentActivity, text, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapterView.OnItemClickListener mAutocompleteClickListener
|
||||||
|
= new AdapterView.OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
final PlaceAutocompleteAdapter.PlaceAutocomplete item = mAdapter.getItem(position);
|
||||||
|
final String placeId = String.valueOf(item.placeId);
|
||||||
|
log.info("Autocomplete item selected: " + item.description);
|
||||||
|
locationApi.getPlaceDetails(placeId, mUpdatePlaceDetailsCallback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private ResultCallback<PlaceBuffer> mUpdatePlaceDetailsCallback
|
||||||
|
= new ResultCallback<PlaceBuffer>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(PlaceBuffer places) {
|
||||||
|
if (places.getStatus().isSuccess()) {
|
||||||
|
final Place place = places.get(0);
|
||||||
|
LatLng latLng = place.getLatLng();
|
||||||
|
Geofence geofence = new Geofence(place.getName().toString(), latLng.latitude, latLng.longitude, 50);
|
||||||
|
log.info("Picked {}", geofence);
|
||||||
|
onLocationPickedHandler.onLocationPicked(geofence);
|
||||||
|
dismiss();
|
||||||
|
} else {
|
||||||
|
error("Error looking up location details - " + places.getStatus().toString());
|
||||||
|
}
|
||||||
|
places.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.ConnectionResult;
|
||||||
|
import com.google.android.gms.common.api.GoogleApiClient;
|
||||||
|
import com.google.android.gms.common.api.PendingResult;
|
||||||
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
|
import com.google.android.gms.common.api.Status;
|
||||||
|
import com.google.android.gms.location.GeofencingRequest;
|
||||||
|
import com.google.android.gms.location.LocationServices;
|
||||||
|
import com.google.common.base.Function;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.tasks.injection.ForApplication;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static com.google.android.gms.location.Geofence.NEVER_EXPIRE;
|
||||||
|
import static com.google.common.collect.Iterables.transform;
|
||||||
|
import static com.google.common.collect.Lists.newArrayList;
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
|
public class GeofenceApi {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GeofenceApi.class);
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private GoogleApiClientProvider googleApiClientProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GeofenceApi(@ForApplication Context context, GoogleApiClientProvider googleApiClientProvider) {
|
||||||
|
this.context = context;
|
||||||
|
this.googleApiClientProvider = googleApiClientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(final List<Geofence> geofences) {
|
||||||
|
if (geofences.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
googleApiClientProvider.getApi(new GoogleApiClientProvider.withApi() {
|
||||||
|
@Override
|
||||||
|
public void doWork(GoogleApiClient googleApiClient) {
|
||||||
|
PendingResult<Status> result = LocationServices.GeofencingApi.addGeofences(
|
||||||
|
googleApiClient,
|
||||||
|
getRequests(geofences),
|
||||||
|
PendingIntent.getService(context, 0, new Intent(context, GeofenceTransitionsIntentService.class), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||||
|
result.setResultCallback(new ResultCallback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(Status status) {
|
||||||
|
if (status.isSuccess()) {
|
||||||
|
log.info("Registered {}", geofences);
|
||||||
|
} else {
|
||||||
|
log.error("Failed to register {}", geofences);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||||
|
log.info("failed to register geofences");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel(final Geofence geofence) {
|
||||||
|
googleApiClientProvider.getApi(new GoogleApiClientProvider.withApi() {
|
||||||
|
@Override
|
||||||
|
public void doWork(GoogleApiClient googleApiClient) {
|
||||||
|
LocationServices.GeofencingApi.removeGeofences(
|
||||||
|
googleApiClient,
|
||||||
|
asList(Long.toString(geofence.getMetadataId())))
|
||||||
|
.setResultCallback(new ResultCallback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(Status status) {
|
||||||
|
if (status.isSuccess()) {
|
||||||
|
log.info("Removed {}", geofence);
|
||||||
|
} else {
|
||||||
|
log.error("Failed to remove {}", geofence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||||
|
log.info("failed to cancel geofence");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<com.google.android.gms.location.Geofence> getRequests(List<Geofence> geofences) {
|
||||||
|
return newArrayList(transform(geofences, new Function<Geofence, com.google.android.gms.location.Geofence>() {
|
||||||
|
@Override
|
||||||
|
public com.google.android.gms.location.Geofence apply(Geofence geofence) {
|
||||||
|
return toGoogleGeofence(geofence);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private com.google.android.gms.location.Geofence toGoogleGeofence(Geofence geofence) {
|
||||||
|
return new com.google.android.gms.location.Geofence.Builder()
|
||||||
|
.setCircularRegion(geofence.getLatitude(), geofence.getLongitude(), geofence.getRadius())
|
||||||
|
.setNotificationResponsiveness((int) TimeUnit.SECONDS.toMillis(30))
|
||||||
|
.setRequestId(Long.toString(geofence.getMetadataId()))
|
||||||
|
.setTransitionTypes(GeofencingRequest.INITIAL_TRIGGER_ENTER)
|
||||||
|
.setExpirationDuration(NEVER_EXPIRE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import com.google.android.gms.location.GeofencingEvent;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.tasks.Broadcaster;
|
||||||
|
import org.tasks.injection.InjectingIntentService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class GeofenceTransitionsIntentService extends InjectingIntentService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GeofenceTransitionsIntentService.class);
|
||||||
|
|
||||||
|
@Inject GeofenceService geofenceService;
|
||||||
|
@Inject Broadcaster broadcaster;
|
||||||
|
|
||||||
|
public GeofenceTransitionsIntentService() {
|
||||||
|
super(GeofenceTransitionsIntentService.class.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
super.onHandleIntent(intent);
|
||||||
|
|
||||||
|
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
|
||||||
|
if (geofencingEvent.hasError()) {
|
||||||
|
log.error("geofence error code {}", geofencingEvent.getErrorCode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int transitionType = geofencingEvent.getGeofenceTransition();
|
||||||
|
|
||||||
|
if (transitionType == com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_ENTER) {
|
||||||
|
List<com.google.android.gms.location.Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
|
||||||
|
log.info("Received geofence transition: {}, {}", transitionType, triggeringGeofences);
|
||||||
|
for (com.google.android.gms.location.Geofence triggerGeofence : triggeringGeofences) {
|
||||||
|
Geofence geofence = geofenceService.getGeofenceById(Long.parseLong(triggerGeofence.getRequestId()));
|
||||||
|
broadcaster.requestNotification(geofence.getMetadataId(), geofence.getTaskId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("invalid geofence transition type: {}", transitionType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.api.GoogleApiClient;
|
||||||
|
import com.google.android.gms.location.LocationServices;
|
||||||
|
import com.google.android.gms.location.places.Places;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.tasks.injection.ForApplication;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class GoogleApiClientProvider {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GoogleApiClientProvider.class);
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public interface withApi extends GoogleApiClient.OnConnectionFailedListener {
|
||||||
|
void doWork(GoogleApiClient googleApiClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GoogleApiClientProvider(@ForApplication Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getApi(final withApi callback) {
|
||||||
|
final GoogleApiClient googleApiClient = new GoogleApiClient.Builder(context)
|
||||||
|
.addOnConnectionFailedListener(callback)
|
||||||
|
.addApi(Places.GEO_DATA_API)
|
||||||
|
.addApi(LocationServices.API)
|
||||||
|
.build();
|
||||||
|
googleApiClient.registerConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
|
||||||
|
@Override
|
||||||
|
public void onConnected(Bundle bundle) {
|
||||||
|
log.info("onConnected({})", bundle);
|
||||||
|
callback.doWork(googleApiClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionSuspended(int i) {
|
||||||
|
log.info("onConnectionSuspended({})", i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
googleApiClient.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.ConnectionResult;
|
||||||
|
import com.google.android.gms.common.api.GoogleApiClient;
|
||||||
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
|
import com.google.android.gms.location.LocationServices;
|
||||||
|
import com.google.android.gms.location.places.AutocompletePredictionBuffer;
|
||||||
|
import com.google.android.gms.location.places.PlaceBuffer;
|
||||||
|
import com.google.android.gms.location.places.Places;
|
||||||
|
import com.google.android.gms.maps.model.LatLng;
|
||||||
|
import com.google.android.gms.maps.model.LatLngBounds;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class LocationApi {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LocationApi.class);
|
||||||
|
|
||||||
|
private GoogleApiClientProvider googleApiClientProvider;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LocationApi(GoogleApiClientProvider googleApiClientProvider) {
|
||||||
|
this.googleApiClientProvider = googleApiClientProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getPlaceDetails(final String placeId, final ResultCallback<PlaceBuffer> callback) {
|
||||||
|
googleApiClientProvider.getApi(new GoogleApiClientProvider.withApi() {
|
||||||
|
@Override
|
||||||
|
public void doWork(final GoogleApiClient googleApiClient) {
|
||||||
|
Places.GeoDataApi.getPlaceById(googleApiClient, placeId)
|
||||||
|
.setResultCallback(new ResultCallback<PlaceBuffer>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(PlaceBuffer places) {
|
||||||
|
callback.onResult(places);
|
||||||
|
googleApiClient.disconnect();
|
||||||
|
}
|
||||||
|
}, 15, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||||
|
log.error("onConnectionFailed({})", connectionResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAutocompletePredictions(final String constraint, final ResultCallback<AutocompletePredictionBuffer> callback) {
|
||||||
|
googleApiClientProvider.getApi(new GoogleApiClientProvider.withApi() {
|
||||||
|
@Override
|
||||||
|
public void doWork(final GoogleApiClient googleApiClient) {
|
||||||
|
final LatLngBounds bounds = LatLngBounds.builder().include(getLastKnownLocation(googleApiClient)).build();
|
||||||
|
Places.GeoDataApi.getAutocompletePredictions(googleApiClient, constraint, bounds, null)
|
||||||
|
.setResultCallback(new ResultCallback<AutocompletePredictionBuffer>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(AutocompletePredictionBuffer autocompletePredictions) {
|
||||||
|
callback.onResult(autocompletePredictions);
|
||||||
|
googleApiClient.disconnect();
|
||||||
|
}
|
||||||
|
}, 15, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||||
|
log.error("onConnectionFailed({})", connectionResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private LatLng getLastKnownLocation(GoogleApiClient googleApiClient) {
|
||||||
|
try {
|
||||||
|
Location lastLocation = LocationServices.FusedLocationApi.getLastLocation(googleApiClient);
|
||||||
|
return new LatLng(lastLocation.getLatitude(), lastLocation.getLongitude());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new LatLng(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
|
import com.google.android.gms.common.api.Status;
|
||||||
|
import com.google.android.gms.location.places.AutocompletePrediction;
|
||||||
|
import com.google.android.gms.location.places.AutocompletePredictionBuffer;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PlaceAutocompleteAdapter
|
||||||
|
extends ArrayAdapter<PlaceAutocompleteAdapter.PlaceAutocomplete> {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PlaceAutocompleteAdapter.class);
|
||||||
|
|
||||||
|
private List<PlaceAutocomplete> mResultList = new ArrayList<>();
|
||||||
|
private LocationApi locationApi;
|
||||||
|
|
||||||
|
public PlaceAutocompleteAdapter(LocationApi locationApi, Context context, int resource) {
|
||||||
|
super(context, resource);
|
||||||
|
this.locationApi = locationApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return mResultList == null ? 0 : mResultList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlaceAutocomplete getItem(int position) {
|
||||||
|
return mResultList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getAutocomplete(CharSequence constraint) {
|
||||||
|
locationApi.getAutocompletePredictions(constraint.toString(), onResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResultCallback<AutocompletePredictionBuffer> onResults = new ResultCallback<AutocompletePredictionBuffer>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(AutocompletePredictionBuffer autocompletePredictions) {
|
||||||
|
final Status status = autocompletePredictions.getStatus();
|
||||||
|
if (!status.isSuccess()) {
|
||||||
|
Toast.makeText(getContext(), "Error contacting API: " + status.toString(),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
log.error("Error getting autocomplete prediction API call: " + status.toString());
|
||||||
|
autocompletePredictions.release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Query completed. Received " + autocompletePredictions.getCount()
|
||||||
|
+ " predictions.");
|
||||||
|
|
||||||
|
Iterator<AutocompletePrediction> iterator = autocompletePredictions.iterator();
|
||||||
|
List<PlaceAutocomplete> resultList = new ArrayList<>(autocompletePredictions.getCount());
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
AutocompletePrediction prediction = iterator.next();
|
||||||
|
resultList.add(new PlaceAutocomplete(prediction.getPlaceId(),
|
||||||
|
prediction.getDescription()));
|
||||||
|
}
|
||||||
|
|
||||||
|
setResults(resultList);
|
||||||
|
autocompletePredictions.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void setResults(List<PlaceAutocomplete> results) {
|
||||||
|
mResultList = results;
|
||||||
|
if (mResultList != null && mResultList.size() > 0) {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
} else {
|
||||||
|
notifyDataSetInvalidated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaceAutocomplete {
|
||||||
|
|
||||||
|
public CharSequence placeId;
|
||||||
|
public CharSequence description;
|
||||||
|
|
||||||
|
PlaceAutocomplete(CharSequence placeId, CharSequence description) {
|
||||||
|
this.placeId = placeId;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return description.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="@dimen/horizontal_page_margin"
|
||||||
|
android:paddingRight="@dimen/horizontal_page_margin"
|
||||||
|
android:paddingTop="@dimen/vertical_page_margin"
|
||||||
|
android:paddingBottom="@dimen/vertical_page_margin">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="top"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:inputType="textPostalAddress"
|
||||||
|
android:imeOptions="actionSearch"
|
||||||
|
android:hint="@string/type_a_location"
|
||||||
|
android:ems="10"
|
||||||
|
android:id="@+id/address_entry" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/powered_by_google_light"
|
||||||
|
android:layout_gravity="end"/>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/list"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<dimen name="horizontal_page_margin">@dimen/margin_huge</dimen>
|
||||||
|
<dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<bool name="sync_enabled">true</bool>
|
<bool name="sync_enabled">true</bool>
|
||||||
|
<bool name="location_enabled">true</bool>
|
||||||
</resources>
|
</resources>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<dimen name="margin_medium">16dp</dimen>
|
||||||
|
<dimen name="margin_huge">64dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="horizontal_page_margin">@dimen/margin_medium</dimen>
|
||||||
|
<dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import com.todoroo.astrid.data.Metadata;
|
||||||
|
|
||||||
|
public class Geofence {
|
||||||
|
private final String name;
|
||||||
|
private final double latitude;
|
||||||
|
private final double longitude;
|
||||||
|
private final int radius;
|
||||||
|
private long taskId;
|
||||||
|
private long metadataId;
|
||||||
|
|
||||||
|
public Geofence(Metadata metadata) {
|
||||||
|
this(metadata.getValue(GeofenceFields.PLACE),
|
||||||
|
metadata.getValue(GeofenceFields.LATITUDE),
|
||||||
|
metadata.getValue(GeofenceFields.LONGITUDE),
|
||||||
|
metadata.getValue(GeofenceFields.RADIUS));
|
||||||
|
metadataId = metadata.getId();
|
||||||
|
taskId = metadata.getTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Geofence(String name, double latitude, double longitude, int radius) {
|
||||||
|
this.name = name;
|
||||||
|
this.latitude = latitude;
|
||||||
|
this.longitude = longitude;
|
||||||
|
this.radius = radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLatitude() {
|
||||||
|
return latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLongitude() {
|
||||||
|
return longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRadius() {
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMetadataId() {
|
||||||
|
return metadataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Geofence{" +
|
||||||
|
"name='" + name + '\'' +
|
||||||
|
", latitude=" + latitude +
|
||||||
|
", longitude=" + longitude +
|
||||||
|
", radius=" + radius +
|
||||||
|
", taskId=" + taskId +
|
||||||
|
", metadataId=" + metadataId +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import com.todoroo.andlib.data.Property.IntegerProperty;
|
||||||
|
import com.todoroo.astrid.data.Metadata;
|
||||||
|
|
||||||
|
import static com.todoroo.andlib.data.Property.DoubleProperty;
|
||||||
|
import static com.todoroo.andlib.data.Property.StringProperty;
|
||||||
|
|
||||||
|
public class GeofenceFields {
|
||||||
|
|
||||||
|
public static final String METADATA_KEY = "geofence";
|
||||||
|
|
||||||
|
public static final StringProperty PLACE = new StringProperty(Metadata.TABLE,
|
||||||
|
Metadata.VALUE1.name);
|
||||||
|
|
||||||
|
public static final DoubleProperty LATITUDE = new DoubleProperty(Metadata.TABLE,
|
||||||
|
Metadata.VALUE2.name);
|
||||||
|
|
||||||
|
public static final DoubleProperty LONGITUDE = new DoubleProperty(Metadata.TABLE,
|
||||||
|
Metadata.VALUE3.name);
|
||||||
|
|
||||||
|
public static final IntegerProperty RADIUS = new IntegerProperty(Metadata.TABLE,
|
||||||
|
Metadata.VALUE4.name);
|
||||||
|
}
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
|
||||||
|
import com.google.common.base.Function;
|
||||||
|
import com.todoroo.andlib.data.Callback;
|
||||||
|
import com.todoroo.andlib.sql.Criterion;
|
||||||
|
import com.todoroo.andlib.sql.Join;
|
||||||
|
import com.todoroo.andlib.sql.Order;
|
||||||
|
import com.todoroo.andlib.sql.Query;
|
||||||
|
import com.todoroo.andlib.utility.DateUtilities;
|
||||||
|
import com.todoroo.astrid.dao.MetadataDao;
|
||||||
|
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
|
||||||
|
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
|
||||||
|
import com.todoroo.astrid.data.Metadata;
|
||||||
|
import com.todoroo.astrid.data.Task;
|
||||||
|
import com.todoroo.astrid.service.SynchronizeMetadataCallback;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import static com.google.common.collect.Iterables.transform;
|
||||||
|
import static com.google.common.collect.Lists.newArrayList;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class GeofenceService {
|
||||||
|
|
||||||
|
private final MetadataDao metadataDao;
|
||||||
|
private final GeofenceApi geofenceApi;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GeofenceService(MetadataDao metadataDao, GeofenceApi geofenceApi) {
|
||||||
|
this.metadataDao = metadataDao;
|
||||||
|
this.geofenceApi = geofenceApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Geofence getGeofenceById(long metadataId) {
|
||||||
|
return new Geofence(metadataDao.fetch(metadataId, Metadata.TASK, GeofenceFields.PLACE, GeofenceFields.LATITUDE, GeofenceFields.LONGITUDE, GeofenceFields.RADIUS));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Geofence> getGeofences(long taskId) {
|
||||||
|
return toGeofences(metadataDao.toList(Query.select(
|
||||||
|
Metadata.PROPERTIES).where(MetadataCriteria.byTaskAndwithKey(
|
||||||
|
taskId, GeofenceFields.METADATA_KEY)).orderBy(Order.asc(GeofenceFields.PLACE))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setupGeofences() {
|
||||||
|
geofenceApi.register(getActiveGeofences());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean synchronizeGeofences(final long taskId, Set<Geofence> geofences) {
|
||||||
|
List<Metadata> metadata = newArrayList(transform(geofences, new Function<Geofence, Metadata>() {
|
||||||
|
@Override
|
||||||
|
public Metadata apply(final Geofence geofence) {
|
||||||
|
return new Metadata() {{
|
||||||
|
setKey(GeofenceFields.METADATA_KEY);
|
||||||
|
setValue(GeofenceFields.PLACE, geofence.getName());
|
||||||
|
setValue(GeofenceFields.LATITUDE, geofence.getLatitude());
|
||||||
|
setValue(GeofenceFields.LONGITUDE, geofence.getLongitude());
|
||||||
|
setValue(GeofenceFields.RADIUS, geofence.getRadius());
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
boolean changed = synchronizeMetadata(taskId, metadata, new SynchronizeMetadataCallback() {
|
||||||
|
@Override
|
||||||
|
public void beforeDeleteMetadata(Metadata m) {
|
||||||
|
geofenceApi.cancel(new Geofence(m));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(changed) {
|
||||||
|
setupGeofences(taskId);
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Geofence> toGeofences(List<Metadata> geofences) {
|
||||||
|
return newArrayList(transform(geofences, new Function<Metadata, Geofence>() {
|
||||||
|
@Override
|
||||||
|
public Geofence apply(Metadata metadata) {
|
||||||
|
return new Geofence(metadata);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupGeofences(long taskId) {
|
||||||
|
geofenceApi.register(getGeofencesForTask(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Geofence> getActiveGeofences() {
|
||||||
|
return toGeofences(metadataDao.toList(Query.select(Metadata.ID, Metadata.TASK, GeofenceFields.PLACE, GeofenceFields.LATITUDE, GeofenceFields.LONGITUDE, GeofenceFields.RADIUS).
|
||||||
|
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
|
||||||
|
where(Criterion.and(TaskCriteria.isActive(), MetadataCriteria.withKey(GeofenceFields.METADATA_KEY)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Geofence> getGeofencesForTask(long taskId) {
|
||||||
|
return toGeofences(metadataDao.toList(Query.select(Metadata.ID, Metadata.TASK, GeofenceFields.PLACE, GeofenceFields.LATITUDE, GeofenceFields.LONGITUDE, GeofenceFields.RADIUS).
|
||||||
|
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
|
||||||
|
where(Criterion.and(TaskCriteria.isActive(),
|
||||||
|
MetadataCriteria.byTaskAndwithKey(taskId, GeofenceFields.METADATA_KEY)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean synchronizeMetadata(long taskId, List<Metadata> metadata, final SynchronizeMetadataCallback callback) {
|
||||||
|
final boolean[] dirty = new boolean[1];
|
||||||
|
final Set<ContentValues> newMetadataValues = new HashSet<>();
|
||||||
|
for(Metadata metadatum : metadata) {
|
||||||
|
metadatum.setTask(taskId);
|
||||||
|
metadatum.clearValue(Metadata.CREATION_DATE);
|
||||||
|
metadatum.clearValue(Metadata.ID);
|
||||||
|
|
||||||
|
ContentValues values = metadatum.getMergedValues();
|
||||||
|
for(Map.Entry<String, Object> entry : values.valueSet()) {
|
||||||
|
if(entry.getKey().startsWith("value")) //$NON-NLS-1$
|
||||||
|
{
|
||||||
|
values.put(entry.getKey(), entry.getValue().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newMetadataValues.add(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataDao.byTaskAndKey(taskId, GeofenceFields.METADATA_KEY, new Callback<Metadata>() {
|
||||||
|
@Override
|
||||||
|
public void apply(Metadata item) {
|
||||||
|
long id = item.getId();
|
||||||
|
|
||||||
|
// clear item id when matching with incoming values
|
||||||
|
item.clearValue(Metadata.ID);
|
||||||
|
item.clearValue(Metadata.CREATION_DATE);
|
||||||
|
ContentValues itemMergedValues = item.getMergedValues();
|
||||||
|
|
||||||
|
if(newMetadataValues.contains(itemMergedValues)) {
|
||||||
|
newMetadataValues.remove(itemMergedValues);
|
||||||
|
} else {
|
||||||
|
// not matched. cut it
|
||||||
|
item.setId(id);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.beforeDeleteMetadata(item);
|
||||||
|
}
|
||||||
|
metadataDao.delete(id);
|
||||||
|
dirty[0] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// everything that remains shall be written
|
||||||
|
for(ContentValues values : newMetadataValues) {
|
||||||
|
Metadata item = new Metadata();
|
||||||
|
item.setCreationDate(DateUtilities.now());
|
||||||
|
item.mergeWith(values);
|
||||||
|
metadataDao.persist(item);
|
||||||
|
dirty[0] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirty[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package org.tasks.location;
|
||||||
|
|
||||||
|
public interface OnLocationPickedHandler {
|
||||||
|
void onLocationPicked(Geofence geofence);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue