Initial support for geofence reminders

* Geofences are not cleaned up when tasks are deleted/completed
* Does not gracefully handle any error conditions
pull/281/head
Alex Baker 11 years ago
parent 11691f37bb
commit a9723e025b

@ -100,6 +100,8 @@ dependencies {
exclude group: 'com.android.support', module: 'support-v4'
}
googleplayCompile group: 'com.google.android.gms', name: 'play-services-location', version: '7.0.0'
compile(group: 'com.google.apis', name: 'google-api-services-tasks', version: 'v1-rev33-1.18.0-rc') {
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}

@ -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() {
}
}

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.tasks">
<!-- **************** -->
@ -16,12 +17,30 @@
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
<!-- ************************ -->
<!-- location based reminders -->
<!-- ************************ -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.LOCATION_HARDWARE" />
<uses-sdk tools:overrideLibrary="com.google.android.gms,com.google.android.gms.location,com.google.android.gms.maps" />
<application>
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBXGYNWNQcfse4JS5gI9teTSKMzinWzL2M"/>
<activity
android:name=".activities.DonationActivity"
android:theme="@style/Tasks.Dialog" />
<service android:name=".location.GeofenceTransitionsIntentService"/>
</application>
</manifest>

@ -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>

@ -82,6 +82,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tasks.R;
import org.tasks.injection.InjectingFragment;
import org.tasks.location.GeofenceService;
import org.tasks.location.LocationApi;
import org.tasks.notifications.NotificationManager;
import org.tasks.preferences.ActivityPreferences;
@ -173,6 +175,8 @@ ViewPager.OnPageChangeListener, EditNoteActivity.UpdatesChangedListener {
@Inject DateChangedAlerts dateChangedAlerts;
@Inject TagDataDao tagDataDao;
@Inject ActFmCameraModule actFmCameraModule;
@Inject GeofenceService geofenceService;
@Inject LocationApi locationApi;
// --- UI components
@ -389,10 +393,9 @@ ViewPager.OnPageChangeListener, EditNoteActivity.UpdatesChangedListener {
controlSetMap.put(getString(R.string.TEA_ctrl_notes_pref),
notesControlSet);
ReminderControlSet reminderControl = new ReminderControlSet(alarmService, this);
controls.add(reminderControl);
controlSetMap.put(getString(R.string.TEA_ctrl_reminders_pref),
reminderControl);
ReminderControlSet reminderControlSet = new ReminderControlSet(locationApi, alarmService, geofenceService, this);
controls.add(reminderControlSet);
controlSetMap.put(getString(R.string.TEA_ctrl_reminders_pref), reminderControlSet);
HideUntilControlSet hideUntilControls = new HideUntilControlSet(this);
controls.add(hideUntilControls);

@ -29,11 +29,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tasks.R;
import org.tasks.dialogs.DateAndTimePickerDialog;
import org.tasks.dialogs.LocationPickerDialog;
import org.tasks.location.Geofence;
import org.tasks.location.GeofenceService;
import org.tasks.location.LocationApi;
import org.tasks.location.OnLocationPickedHandler;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static org.tasks.date.DateTimeUtils.newDateTime;
@ -55,15 +61,19 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte
private LinearLayout alertContainer;
private boolean whenDue;
private boolean whenOverdue;
private LocationApi locationApi;
private AlarmService alarmService;
private GeofenceService geofenceService;
private TaskEditFragment taskEditFragment;
private List<String> spinnerOptions = new ArrayList<>();
private ArrayAdapter<String> remindAdapter;
public ReminderControlSet(AlarmService alarmService, TaskEditFragment taskEditFragment) {
public ReminderControlSet(LocationApi locationApi, AlarmService alarmService, GeofenceService geofenceService, TaskEditFragment taskEditFragment) {
super(taskEditFragment.getActivity(), R.layout.control_set_reminders);
this.locationApi = locationApi;
this.alarmService = alarmService;
this.geofenceService = geofenceService;
this.taskEditFragment = taskEditFragment;
}
@ -117,6 +127,15 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte
});
}
public void addGeolocationReminder(final Geofence geofence) {
View alertItem = addAlarmRow(geofence.getName(), null, new OnClickListener() {
@Override
public void onClick(View v) {
}
});
alertItem.setTag(geofence);
}
private View addAlarmRow(String text, Long timestamp, final OnClickListener onRemove) {
final View alertItem = LayoutInflater.from(activity).inflate(R.layout.alarm_edit_row, null);
alertContainer.addView(alertItem);
@ -155,6 +174,9 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte
if (randomControlSet == null) {
spinnerOptions.add(taskEditFragment.getString(R.string.randomly));
}
if (taskEditFragment.getResources().getBoolean(R.bool.location_enabled)) {
spinnerOptions.add(taskEditFragment.getString(R.string.pick_a_location));
}
spinnerOptions.add(taskEditFragment.getString(R.string.pick_a_date_and_time));
remindAdapter.notifyDataSetChanged();
}
@ -257,6 +279,9 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte
addAlarmRow(entry.getValue(AlarmFields.TIME));
}
});
for (Geofence geofence : geofenceService.getGeofences(model.getId())) {
addGeolocationReminder(geofence);
}
updateSpinner();
}
@ -310,18 +335,28 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte
task.setReminderPeriod(randomControlSet == null ? 0L : randomControlSet.getReminderPeriod());
LinkedHashSet<Long> alarms = new LinkedHashSet<>();
Set<Long> alarms = new LinkedHashSet<>();
Set<Geofence> geofences = new LinkedHashSet<>();
for(int i = 0; i < alertContainer.getChildCount(); i++) {
Long dateValue = (Long) alertContainer.getChildAt(i).getTag();
if(dateValue == null) {
continue;
Object tag = alertContainer.getChildAt(i).getTag();
//noinspection StatementWithEmptyBody
if (tag == null) {
} else if (tag instanceof Long) {
alarms.add((Long) tag);
} else if (tag instanceof Geofence) {
geofences.add((Geofence) tag);
} else {
log.error("Unexpected tag: {}", tag);
}
alarms.add(dateValue);
}
if(alarmService.synchronizeAlarms(task.getId(), alarms)) {
task.setModificationDate(DateUtilities.now());
}
if (geofenceService.synchronizeGeofences(task.getId(), geofences)) {
task.setModificationDate(DateUtilities.now());
}
}
private String getDisplayString(long forDate) {
@ -345,6 +380,13 @@ public class ReminderControlSet extends TaskEditControlSetBase implements Adapte
addRandomReminder();
} else if (selected.equals(taskEditFragment.getString(R.string.pick_a_date_and_time))) {
addNewAlarm();
} else if (selected.equals(taskEditFragment.getString(R.string.pick_a_location))) {
LocationPickerDialog.pickLocation(locationApi, taskEditFragment, new OnLocationPickedHandler() {
@Override
public void onLocationPicked(Geofence geofence) {
addGeolocationReminder(geofence);
}
});
}
if (position != 0) {
updateSpinner();

@ -6,6 +6,7 @@ import android.content.Intent;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.reminders.NotificationFragment;
import com.todoroo.astrid.reminders.Notifications;
import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.utility.Constants;
import org.tasks.injection.ForApplication;
@ -55,6 +56,14 @@ public class Broadcaster {
}}, AstridApiConstants.PERMISSION_READ);
}
public void requestNotification(final long alarmId, final long taskId) {
sendOrderedBroadcast(new Intent(context, Notifications.class) {{
setAction("ALARM" + alarmId); //$NON-NLS-1$
putExtra(Notifications.ID_KEY, taskId);
putExtra(Notifications.EXTRAS_TYPE, ReminderService.TYPE_ALARM);
}});
}
public void taskCompleted(final long id) {
sendOrderedBroadcast(new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_COMPLETED) {{
putExtra(AstridApiConstants.EXTRAS_TASK_ID, id);

@ -3,6 +3,7 @@ package org.tasks.injection;
import android.app.Activity;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import com.todoroo.astrid.actfm.TagViewFragment;
import com.todoroo.astrid.activity.TaskEditFragment;

@ -1,5 +1,6 @@
package org.tasks.injection;
import org.tasks.location.GeofenceTransitionsIntentService;
import org.tasks.scheduling.*;
import dagger.Module;
@ -11,7 +12,8 @@ import dagger.Module;
GtasksBackgroundService.class,
MidnightRefreshService.class,
RefreshSchedulerIntentService.class,
ReminderSchedulerIntentService.class
ReminderSchedulerIntentService.class,
GeofenceTransitionsIntentService.class
})
public class IntentServiceModule {
}

@ -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);
}

@ -7,6 +7,7 @@ import com.todoroo.astrid.alarms.AlarmService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tasks.injection.InjectingIntentService;
import org.tasks.location.GeofenceService;
import javax.inject.Inject;
@ -15,6 +16,7 @@ public class AlarmSchedulingIntentService extends InjectingIntentService {
private static final Logger log = LoggerFactory.getLogger(AlarmSchedulingIntentService.class);
@Inject AlarmService alarmService;
@Inject GeofenceService geofenceService;
public AlarmSchedulingIntentService() {
super(AlarmSchedulingIntentService.class.getSimpleName());
@ -27,5 +29,6 @@ public class AlarmSchedulingIntentService extends InjectingIntentService {
log.debug("onHandleIntent({})", intent);
alarmService.scheduleAllAlarms();
geofenceService.setupGeofences();
}
}

@ -494,6 +494,8 @@
<string name="randomly_once">Произволно веднъж</string>
<string name="randomly">Произволно</string>
<string name="pick_a_date_and_time">Избери дата и време</string>
<string name="pick_a_location">Избери местоположение</string>
<string name="type_a_location">Въведи местоположение</string>
<string name="when_overdue">След крайния срок</string>
<string name="when_due">При краен срок</string>
<string-array name="sync_SPr_interval_entries">

@ -492,6 +492,8 @@
<string name="randomly_once">1回ランダムに</string>
<string name="randomly">ランダムに</string>
<string name="pick_a_date_and_time">日付と時間を選択</string>
<string name="pick_a_location">場所を選択</string>
<string name="type_a_location">場所を入力</string>
<string name="when_overdue">期限を過ぎたとき</string>
<string name="when_due">期限に</string>
<string-array name="sync_SPr_interval_entries">

@ -4,4 +4,5 @@
<bool name="at_least_honeycomb_mr1">false</bool>
<bool name="billing_enabled">false</bool>
<bool name="sync_enabled">false</bool>
<bool name="location_enabled">false</bool>
</resources>

@ -98,6 +98,8 @@
<string name="randomly_once">Randomly once</string>
<string name="randomly">Randomly</string>
<string name="pick_a_date_and_time">Pick a date and time</string>
<string name="pick_a_location">Pick a location</string>
<string name="type_a_location">Type a location</string>
<string name="when_overdue">When overdue</string>
<string name="when_due">When due</string>

Loading…
Cancel
Save