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"?>
|
||||
<resources>
|
||||
<bool name="sync_enabled">true</bool>
|
||||
<bool name="location_enabled">true</bool>
|
||||
</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