/* * Copyright (C) 2011 - 2013 Niall 'Rivernile' Scott * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors or contributors be held liable for * any damages arising from the use of this software. * * The aforementioned copyright holder(s) hereby grant you a * non-transferrable right to use this software for any purpose (including * commercial applications), and to modify it and redistribute it, subject to * the following conditions: * * 1. This notice may not be removed or altered from any file it appears in. * * 2. Any modifications made to this software, except those defined in * clause 3 of this agreement, must be released under this license, and * the source code of any modifications must be made available on a * publically accessible (and locateable) website, or sent to the * original author of this software. * * 3. Software modifications that do not alter the functionality of the * software but are simply adaptations to a specific environment are * exempt from clause 2. */ package uk.org.rivernile.edinburghbustracker.android.alerts; import android.app.AlarmManager; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Intent; import android.content.SharedPreferences; import android.os.SystemClock; import android.support.v4.app.NotificationCompat; import java.util.HashMap; import uk.org.rivernile.android.bustracker.parser.livetimes.BusParser; import uk.org.rivernile.android.bustracker.parser.livetimes.BusParserException; import uk.org.rivernile.android.bustracker.parser.livetimes.BusService; import uk.org.rivernile.android.bustracker.parser.livetimes.BusStop; import uk.org.rivernile.edinburghbustracker.android.BusStopDatabase; import uk.org.rivernile.edinburghbustracker.android.DisplayStopDataActivity; import uk.org.rivernile.edinburghbustracker.android.PreferencesActivity; import uk.org.rivernile.edinburghbustracker.android.R; import uk.org.rivernile.edinburghbustracker.android.SettingsDatabase; import uk.org.rivernile.edinburghbustracker.android.livetimes.parser.EdinburghBus; import uk.org.rivernile.edinburghbustracker.android.livetimes.parser .EdinburghParser; /** * The purpose of the TimeAlertService is run on a once-per-minute basis to load * bus times from the server to see if any of the services the user has filtered * on have arrived at the bus stop within the time trigger time, also set by the * user. If the criteria is not met then it schedules to run again in the next * minute. If the criteria is met, the user is greeted with a notification. * * As this is an IntentService, it runs in a separate thread and does not block * the UI thread. * * @author Niall Scott */ public class TimeAlertService extends IntentService { /** Argument for the stopCode. */ public static final String ARG_STOPCODE = "stopCode"; /** Argument for the list of services to check against. */ public static final String ARG_SERVICES = "services"; /** Argument for the time threshold to trigger an alert. */ public static final String ARG_TIME_TRIGGER = "timeTrigger"; /** Argument for the time that the alert was set at. */ public static final String ARG_TIME_SET = "timeSet"; private static final int ALERT_ID = 2; private SettingsDatabase sd; private BusStopDatabase bsd; private NotificationManager notifMan; private AlertManager alertMan; private AlarmManager alarmMan; private SharedPreferences sp; /** * Create a new instance of the TimeAlertService. This simply calls its * super constructor. */ public TimeAlertService() { super(TimeAlertService.class.getSimpleName()); } /** * {@inheritDoc} */ @Override public void onCreate() { super.onCreate(); sd = SettingsDatabase.getInstance(getApplicationContext()); bsd = BusStopDatabase.getInstance(getApplicationContext()); notifMan = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); alertMan = AlertManager.getInstance(this); alarmMan = (AlarmManager)getSystemService(ALARM_SERVICE); sp = getSharedPreferences(PreferencesActivity.PREF_FILE, 0); } /** * {@inheritDoc} */ @Override protected void onHandleIntent(final Intent intent) { final String stopCode = intent.getStringExtra(ARG_STOPCODE); final String[] services = intent.getStringArrayExtra(ARG_SERVICES); final int timeTrigger = intent.getIntExtra(ARG_TIME_TRIGGER, 5); final BusParser parser = new EdinburghParser(); HashMap<String, BusStop> result; try { // Get the bus times. Only get 1 bus per service. result = parser.getBusStopData(new String[] { stopCode }, 1); } catch(BusParserException e) { // There was an error. No point continuing. Reschedule. reschedule(intent); return; } // Get the bus stop we are interested in. It should be the only one in // the HashMap anyway. final BusStop busStop = result.get(stopCode); int time; EdinburghBus edinBs; // Loop through all the bus services at this stop. for(BusService bs : busStop.getBusServices()) { // We are only interested in the next departure. Also get the time. edinBs = (EdinburghBus)bs.getFirstBus(); if (edinBs == null) { continue; } time = edinBs.getArrivalMinutes(); // Loop through all of the services we are interested in. for(String service : services) { // The service matches and meets the time criteria. if(service.equals(bs.getServiceName()) && time <= timeTrigger) { // The alert may have been cancelled by the user recently, // check it's still active to stay relevant. Cancel the // alert if we're continuing. if(!sd.isActiveTimeAlert(stopCode)) return; alertMan.removeTimeAlert(); // Create the intent that's fired when the notification is // tapped. It shows the bus times view for that stop. final Intent launchIntent = new Intent(this, DisplayStopDataActivity.class); launchIntent.setAction(DisplayStopDataActivity .ACTION_VIEW_STOP_DATA); launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); launchIntent.putExtra(DisplayStopDataActivity.ARG_STOPCODE, stopCode); launchIntent.putExtra(DisplayStopDataActivity.ARG_FORCELOAD, true); final String stopName = bsd.getNameForBusStop(stopCode); final String title = getString(R.string .timeservice_notification_title); final String summary = getResources().getQuantityString( R.plurals.timeservice_notification_summary, time == 0 ? 1 : time, service, time, stopName); // Build the notification. final NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(this); notifBuilder.setAutoCancel(true); notifBuilder.setSmallIcon(R.drawable.ic_status_bus); notifBuilder.setTicker(summary); notifBuilder.setContentTitle(title); notifBuilder.setContentText(summary); // Support for Jelly Bean notifications. notifBuilder.setStyle(new NotificationCompat.BigTextStyle() .bigText(summary)); notifBuilder.setContentIntent( PendingIntent.getActivity(this, 0, launchIntent, PendingIntent.FLAG_ONE_SHOT)); final Notification n = notifBuilder.build(); if(sp.getBoolean(PreferencesActivity.PREF_ALERT_SOUND, true)) n.defaults |= Notification.DEFAULT_SOUND; if(sp.getBoolean(PreferencesActivity.PREF_ALERT_VIBRATE, true)) n.defaults |= Notification.DEFAULT_VIBRATE; if(sp.getBoolean(PreferencesActivity.PREF_ALERT_LED, true)) { n.defaults |= Notification.DEFAULT_LIGHTS; n.flags |= Notification.FLAG_SHOW_LIGHTS; } // Send the notification. notifMan.notify(ALERT_ID, n); return; } } } // All the services have been looped through and the criteria didn't // match. This means a reschedule should be attempted. reschedule(intent); } /** * Reschedule the retrieval of bus times from the server because there was * an error loading them or the service/time criteria has not been met. * * If the rescheduling goes on for an hour, then cancel the checking and * remove the alert otherwise the user's battery will be drained and data * used. * * @param intent The intent that started this service. This is to be reused * to start the next service at the appropriate time. */ private void reschedule(final Intent intent) { final long timeSet = intent.getLongExtra(ARG_TIME_SET, 0); // Checks to see if the alert has been active for the last hour or more. // If so, it gets cancelled. if((SystemClock.elapsedRealtime() - timeSet) >= 3600000) { alertMan.removeTimeAlert(); return; } final PendingIntent pi = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); // Reschedule ourself to run again in 60 seconds. alarmMan.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 60000, pi); } }