/**
* Copyright 2010 Mark Wyszomierski
*/
package com.joelapenna.foursquared.app;
import com.joelapenna.foursquare.Foursquare;
import com.joelapenna.foursquare.types.Checkin;
import com.joelapenna.foursquare.types.Group;
import com.joelapenna.foursquare.types.User;
import com.joelapenna.foursquared.Foursquared;
import com.joelapenna.foursquared.FriendsActivity;
import com.joelapenna.foursquared.MainActivity;
import com.joelapenna.foursquared.R;
import com.joelapenna.foursquared.location.LocationUtils;
import com.joelapenna.foursquared.preferences.Preferences;
import com.joelapenna.foursquared.util.StringFormatters;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;
import android.widget.RemoteViews;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* This service will run every N minutes (specified by the user in settings). An alarm
* handles running the service.
*
* When the service runs, we call /checkins. From the list of checkins, we cut down the
* list of relevant checkins as follows:
* <ul>
* <li>Not one of our own checkins.</li>
* <li>We haven't turned pings off for the user. This can be toggled on/off in the
* UserDetailsActivity activity, per user.</li>
* <li>The checkin is younger than the last time we ran this service.</li>
* </ul>
*
* Note that the server might override the pings attribute to 'off' for certain checkins,
* usually if the checkin is far away from our current location.
*
* Pings will not be cleared from the notification bar until a subsequent run can
* generate at least one new ping. A new batch of pings will clear all
* previous pings so as to not clutter the notification bar.
*
* @date May 21, 2010
* @author Mark Wyszomierski (markww@gmail.com)
*/
public class PingsService extends WakefulIntentService {
public static final String TAG = "PingsService";
private static final boolean DEBUG = false;
public static final int NOTIFICATION_ID_CHECKINS = 15;
public PingsService() {
super("PingsService");
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void doWakefulWork(Intent intent) {
Log.i(TAG, "Foursquare pings service running...");
// The user must have logged in once previously for this to work,
// and not leave the app in a logged-out state.
Foursquared foursquared = (Foursquared) getApplication();
Foursquare foursquare = foursquared.getFoursquare();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (!foursquared.isReady()) {
Log.i(TAG, "User not logged in, cannot proceed.");
return;
}
// Before running, make sure the user still wants pings on.
// For example, the user could have turned pings on from
// this device, but then turned it off on a second device. This
// service would continue running then, continuing to notify the
// user.
if (!checkUserStillWantsPings(foursquared.getUserId(), foursquare)) {
// Turn off locally.
Log.i(TAG, "Pings have been turned off for user, cancelling service.");
prefs.edit().putBoolean(Preferences.PREFERENCE_PINGS, false).commit();
cancelPings(this);
return;
}
// Get the users current location and then request nearby checkins.
Group<Checkin> checkins = null;
Location location = getLastGeolocation();
if (location != null) {
try {
checkins = foursquare.checkins(
LocationUtils.createFoursquareLocation(location));
} catch (Exception ex) {
Log.e(TAG, "Error getting checkins in pings service.", ex);
}
} else {
Log.e(TAG, "Could not find location in pings service, cannot proceed.");
}
if (checkins == null) {
// unsuccessful attempt; go ahead and abort and don't update any state
return;
}
Log.i(TAG, "Checking " + checkins.size() + " checkins for pings.");
// Don't accept any checkins that are older than the last time we ran.
long lastRunTime = prefs.getLong(
Preferences.PREFERENCE_PINGS_SERVICE_LAST_RUN_TIME, System.currentTimeMillis());
Date dateLast = new Date(lastRunTime);
Log.i(TAG, "Last service run time: " + dateLast.toLocaleString() + " (" + lastRunTime + ").");
// Now build the list of 'new' checkins.
List<Checkin> newCheckins = new ArrayList<Checkin>();
for (Checkin it : checkins) {
if (DEBUG) Log.d(TAG, "Checking checkin of " + it.getUser().getFirstname());
// Ignore ourselves. The server should handle this by setting the pings flag off but..
if (it.getUser() != null && it.getUser().getId().equals(foursquared.getUserId())) {
if (DEBUG) Log.d(TAG, " Ignoring checkin of ourselves.");
continue;
}
// Check that our user wanted to see pings from this user.
if (!it.getPing()) {
if (DEBUG) Log.d(TAG, " Pings are off for this user.");
continue;
}
// Check against date times.
try {
Date dateCheckin = StringFormatters.DATE_FORMAT.parse(it.getCreated());
if (DEBUG) {
Log.d(TAG, " Comaring date times for checkin.");
Log.d(TAG, " Last run time: " + dateLast.toLocaleString());
Log.d(TAG, " Checkin time: " + dateCheckin.toLocaleString());
}
// If it's an 'off the grid' checkin, ignore.
if (it.getVenue() == null && it.getShout() == null) {
if (DEBUG) Log.d(TAG, " Checkin is off the grid, ignoring.");
continue;
}
if (dateCheckin.after(dateLast)) {
if (DEBUG) Log.d(TAG, " Checkin is younger than our last run time, passes all tests!!");
newCheckins.add(it);
} else {
if (DEBUG) Log.d(TAG, " Checkin is older than last run time.");
}
} catch (ParseException ex) {
if (DEBUG) Log.e(TAG, " Error parsing checkin timestamp: " + it.getCreated(), ex);
}
}
Log.i(TAG, "Found " + newCheckins.size() + " new checkins.");
if ( newCheckins.size() > 0 &&
PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Preferences.PREFERENCE_SYNC_CONTACTS, false)) {
ContentResolver resolver = getApplication().getContentResolver();
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(newCheckins.size());
for ( Checkin checkin : newCheckins) {
ops.addAll(foursquared.getSync().updateStatus(resolver, checkin.getUser(), checkin));
}
try {
resolver.applyBatch(ContactsContract.AUTHORITY, ops);
} catch (Exception e) {
Log.w(TAG, "updating contact statuses failed", e);
}
Log.i(TAG, "Found " + newCheckins.size() + " new checkins.");
notifyUser(newCheckins);
} else {
// Checkins were null, so don't record this as the last run time in order to try
// fetching checkins we may have missed on the next run.
// Thanks to logan.johnson@gmail.com for the fix.
Log.i(TAG, "Checkins were null, won't update last run timestamp.");
return;
}
// Record this as the last time we ran.
prefs.edit().putLong(
Preferences.PREFERENCE_PINGS_SERVICE_LAST_RUN_TIME, System.currentTimeMillis()).commit();
}
private Location getLastGeolocation() {
LocationManager manager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
List<String> providers = manager.getAllProviders();
Location bestLocation = null;
for (String it : providers) {
Location location = manager.getLastKnownLocation(it);
if (location != null) {
if (bestLocation == null || location.getAccuracy() < bestLocation.getAccuracy()) {
bestLocation = location;
}
}
}
return bestLocation;
}
private void notifyUser(List<Checkin> newCheckins) {
// If we have no new checkins to show, nothing to do. We would also be leaving the
// previous batch of pings alive (if any) which is ok.
if (newCheckins.size() < 1) {
return;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
// Clear all previous pings notifications before showing new ones.
NotificationManager mgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
mgr.cancelAll();
// We'll only ever show a single entry, so we collapse data depending on how many
// new checkins we received on this refresh.
RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.pings_list_item);
if (newCheckins.size() == 1) {
// A single checkin, show full checkin preview.
Checkin checkin = newCheckins.get(0);
String checkinMsgLine1 = StringFormatters.getCheckinMessageLine1(checkin, true);
String checkinMsgLine2 = StringFormatters.getCheckinMessageLine2(checkin);
String checkinMsgLine3 = StringFormatters.getCheckinMessageLine3(checkin);
contentView.setTextViewText(R.id.text1, checkinMsgLine1);
if (!TextUtils.isEmpty(checkinMsgLine2)) {
contentView.setTextViewText(R.id.text2, checkinMsgLine2);
contentView.setTextViewText(R.id.text3, checkinMsgLine3);
} else {
contentView.setTextViewText(R.id.text2, checkinMsgLine3);
}
} else {
// More than one new checkin, collapse them.
String checkinMsgLine1 = newCheckins.size() + " new Foursquare checkins!";
StringBuilder sbCheckinMsgLine2 = new StringBuilder(1024);
for (Checkin it : newCheckins) {
sbCheckinMsgLine2.append(StringFormatters.getUserAbbreviatedName(it.getUser()));
sbCheckinMsgLine2.append(", ");
}
if (sbCheckinMsgLine2.length() > 0) {
sbCheckinMsgLine2.delete(sbCheckinMsgLine2.length()-2, sbCheckinMsgLine2.length());
}
String checkinMsgLine3 = "at " + StringFormatters.DATE_FORMAT_TODAY.format(new Date());
contentView.setTextViewText(R.id.text1, checkinMsgLine1);
contentView.setTextViewText(R.id.text2, sbCheckinMsgLine2.toString());
contentView.setTextViewText(R.id.text3, checkinMsgLine3);
}
PendingIntent pi = PendingIntent.getActivity(this, 0, new Intent(this, FriendsActivity.class), 0);
Notification notification = new Notification(
R.drawable.notification_icon,
"Foursquare Checkin",
System.currentTimeMillis());
notification.contentView = contentView;
notification.contentIntent = pi;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
if (prefs.getBoolean(Preferences.PREFERENCE_PINGS_VIBRATE, false)) {
notification.defaults |= Notification.DEFAULT_VIBRATE;
}
if (prefs.getBoolean(Preferences.PREFERENCE_PINGS_FLASH, false)) {
notification.ledARGB = 0xff73206b;
notification.ledOnMS = 500; // com.android.internal.R.integer.config_defaultNotificationLedOn
notification.ledOffMS = 2000; // com.android.internal.R.integer.config_defaultNotificationLedOff
notification.flags |= Notification.FLAG_SHOW_LIGHTS;
}
if (newCheckins.size() > 1) {
notification.number = newCheckins.size();
}
mgr.notify(NOTIFICATION_ID_CHECKINS, notification);
}
private boolean checkUserStillWantsPings(String userId, Foursquare foursquare) {
try {
User user = foursquare.user(userId, false, false, null);
if (user != null) {
return user.getSettings().getPings().equals("on");
}
} catch (Exception ex) {
// Assume they still want it on.
}
return true;
}
public static void setupPings(Context context) {
// If the user has pings on, set an alarm every N minutes, where N is their
// requested refresh rate. We default to 30 if some problem reading set interval.
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (prefs.getBoolean(Preferences.PREFERENCE_PINGS, false)) {
int refreshRateInMinutes = getRefreshIntervalInMinutes(prefs);
if (DEBUG) {
Log.d(TAG, "User has pings on, attempting to setup alarm with interval: "
+ refreshRateInMinutes + "..");
}
// We want to mark this as the last run time so we don't get any notifications
// before the service is started.
prefs.edit().putLong(
Preferences.PREFERENCE_PINGS_SERVICE_LAST_RUN_TIME, System.currentTimeMillis()).commit();
// Schedule the alarm.
AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + (refreshRateInMinutes * 60 * 1000),
refreshRateInMinutes * 60 * 1000,
makePendingIntentAlarm(context));
}
}
public static void cancelPings(Context context) {
AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
mgr.cancel(makePendingIntentAlarm(context));
}
private static PendingIntent makePendingIntentAlarm(Context context) {
return PendingIntent.getBroadcast(context, 0, new Intent(context, PingsOnAlarmReceiver.class), 0);
}
public static void clearAllNotifications(Context context) {
NotificationManager mgr = (NotificationManager)context.getSystemService(NOTIFICATION_SERVICE);
mgr.cancelAll();
}
public static void generatePingsTest(Context context) {
Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
NotificationManager mgr = (NotificationManager)context.getSystemService(NOTIFICATION_SERVICE);
RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.pings_list_item);
contentView.setTextViewText(R.id.text1, "Ping title line");
contentView.setTextViewText(R.id.text2, "Ping message line 2");
contentView.setTextViewText(R.id.text3, "Ping message line 3");
Notification notification = new Notification(
R.drawable.notification_icon,
"Foursquare Checkin",
System.currentTimeMillis());
notification.contentView = contentView;
notification.contentIntent = pi;
notification.defaults |= Notification.DEFAULT_VIBRATE;
mgr.notify(-1, notification);
}
private static int getRefreshIntervalInMinutes(SharedPreferences prefs) {
int refreshRateInMinutes = 30;
try {
refreshRateInMinutes = Integer.parseInt(prefs.getString(
Preferences.PREFERENCE_PINGS_INTERVAL, String.valueOf(refreshRateInMinutes)));
} catch (NumberFormatException ex) {
Log.e(TAG, "Error parsing pings interval time, defaulting to: " + refreshRateInMinutes);
}
return refreshRateInMinutes;
}
}