BartRunnerAndroid/src/com/dougkeen/bart/services/BoardedDepartureService.java
Doug Keen 5836ccb89c Added alarm indicator to Your Train card.
Fixed mysterious "freeze" of notifications (turned out to be unexpected gc of weak references).
Departure countdown now reads "Leaving" at the end of the countdown.
2012-10-10 17:21:30 -07:00

317 lines
8.4 KiB
Java

package com.dougkeen.bart.services;
import java.lang.ref.WeakReference;
import java.util.List;
import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.model.Departure;
import com.dougkeen.bart.model.StationPair;
import com.dougkeen.bart.services.EtdService.EtdServiceBinder;
import com.dougkeen.bart.services.EtdService.EtdServiceListener;
import com.dougkeen.util.Observer;
public class BoardedDepartureService extends Service implements
EtdServiceListener {
private static final int DEPARTURE_NOTIFICATION_ID = 123;
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private boolean mBound = false;
private EtdService mEtdService;
private StationPair mStationPair;
private NotificationManager mNotificationManager;
private AlarmManager mAlarmManager;
private Handler mHandler;
private boolean mHasShutDown = false;
public BoardedDepartureService() {
super();
}
private static final class ServiceHandler extends Handler {
private final WeakReference<BoardedDepartureService> mServiceRef;
public ServiceHandler(Looper looper,
BoardedDepartureService boardedDepartureService) {
super(looper);
mServiceRef = new WeakReference<BoardedDepartureService>(
boardedDepartureService);
}
@Override
public void handleMessage(Message msg) {
BoardedDepartureService service = mServiceRef.get();
if (service != null) {
service.onHandleIntent((Intent) msg.obj);
}
}
}
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
mEtdService = null;
mBound = false;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mEtdService = ((EtdServiceBinder) service).getService();
if (getStationPair() != null) {
mEtdService.registerListener(BoardedDepartureService.this,
false);
}
mBound = true;
}
};
@Override
public void onCreate() {
HandlerThread thread = new HandlerThread(
"BartRunnerNotificationService");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper, this);
bindService(new Intent(this, EtdService.class), mConnection,
Context.BIND_AUTO_CREATE);
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mHandler = new Handler();
super.onCreate();
}
@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mHasShutDown = false;
onStart(intent, startId);
return START_STICKY;
}
@Override
public void onDestroy() {
shutDown(true);
if (mBound)
unbindService(mConnection);
mServiceLooper.quit();
super.onDestroy();
}
protected void onHandleIntent(Intent intent) {
final BartRunnerApplication application = (BartRunnerApplication) getApplication();
final Departure boardedDeparture = application.getBoardedDeparture();
if (boardedDeparture == null || intent == null) {
// Nothing to notify about
return;
}
if (intent.getBooleanExtra("cancelNotifications", false)
|| intent.getBooleanExtra("clearBoardedDeparture", false)) {
// We want to cancel the alarm
boardedDeparture
.cancelAlarm(getApplicationContext(), mAlarmManager);
if (intent.getBooleanExtra("clearBoardedDeparture", false)) {
application.setBoardedDeparture(null);
shutDown(false);
} else {
updateNotification();
}
return;
}
StationPair oldStationPair = mStationPair;
mStationPair = boardedDeparture.getStationPair();
if (mEtdService != null && mStationPair != null
&& !mStationPair.equals(oldStationPair)) {
mEtdService.unregisterListener(this);
}
if (getStationPair() != null && mEtdService != null) {
mEtdService.registerListener(this, false);
}
boardedDeparture.getAlarmLeadTimeMinutesObservable().registerObserver(
new Observer<Integer>() {
@Override
public void onUpdate(Integer newValue) {
updateNotification();
}
});
boardedDeparture.getAlarmPendingObservable().registerObserver(
new Observer<Boolean>() {
@Override
public void onUpdate(Boolean newValue) {
updateNotification();
}
});
updateNotification();
pollDepartureStatus();
}
private void updateAlarm() {
Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture();
if (boardedDeparture != null) {
boardedDeparture
.updateAlarm(getApplicationContext(), mAlarmManager);
}
}
@Override
public void onETDChanged(List<Departure> departures) {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture();
for (Departure departure : departures) {
if (departure.equals(boardedDeparture)
&& (boardedDeparture.getMeanSecondsLeft() != departure
.getMeanSecondsLeft() || boardedDeparture
.getUncertaintySeconds() != departure
.getUncertaintySeconds())) {
boardedDeparture.mergeEstimate(departure);
// Also merge back, in case boardedDeparture estimate is better
departure.mergeEstimate(boardedDeparture);
updateAlarm();
break;
}
}
}
@Override
public void onError(String errorMessage) {
// Do nothing
}
@Override
public void onRequestStarted() {
// Do nothing
}
@Override
public void onRequestEnded() {
// Do nothing
}
@Override
public StationPair getStationPair() {
return mStationPair;
}
private long mNextScheduledCheckClockTime = 0;
private void pollDepartureStatus() {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture();
if (boardedDeparture == null || boardedDeparture.hasDeparted()) {
shutDown(false);
return;
}
if (mEtdService != null) {
/*
* Make sure we're still listening for ETD changes (in case weak ref
* was garbage collected). Not a huge fan of this approach, but I
* think I'd rather keep the weak references to avoid memory leaks
* than move to soft references or some other form of stronger
* reference. Besides, registerListener() should only result in a
* few constant-time map operations, so there shouldn't be a big
* performance hit.
*/
mEtdService.registerListener(this, false);
}
boardedDeparture.updateAlarm(getApplicationContext(), mAlarmManager);
updateNotification();
final int pollIntervalMillis = getPollIntervalMillis();
final long scheduledCheckClockTime = System.currentTimeMillis()
+ pollIntervalMillis;
if (mNextScheduledCheckClockTime < scheduledCheckClockTime) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
pollDepartureStatus();
}
}, pollIntervalMillis);
mNextScheduledCheckClockTime = scheduledCheckClockTime;
}
}
private void shutDown(boolean isBeingDestroyed) {
if (!mHasShutDown) {
mHasShutDown = true;
if (mEtdService != null) {
mEtdService.unregisterListener(this);
}
if (mNotificationManager != null) {
mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID);
}
if (!isBeingDestroyed)
stopSelf();
}
}
private void updateNotification() {
if (mHasShutDown) {
if (mEtdService != null) {
mEtdService.unregisterListener(this);
}
return;
}
final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture();
if (boardedDeparture != null) {
mNotificationManager.notify(DEPARTURE_NOTIFICATION_ID,
boardedDeparture
.createNotification(getApplicationContext()));
}
}
private int getPollIntervalMillis() {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture();
if (boardedDeparture.getSecondsUntilAlarm() > 3 * 60) {
return 15 * 1000;
} else {
return 6 * 1000;
}
}
@Override
public IBinder onBind(Intent intent) {
// Doesn't support binding
return null;
}
}