/* * Copyright (C) 2016 Paul Watts (paulcwatts@gmail.com), * University of South Florida (sjbarbeau@gmail.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onebusaway.android.tripservice; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.util.Log; import org.onebusaway.android.BuildConfig; import org.onebusaway.android.provider.ObaContract; import org.onebusaway.android.util.UIUtils; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * A container Service for a thread pool that manages the scheduling, polling, and notifying the * user of an upcoming bus at their stop.. The thread pool can contain executing threads for * various * tasks related to the reminders feature - the SchedulerTask (executed when a new reminder is * saved, or after boot (via BootstrapService) to register all reminders saved in the database with * the Android platform), the PollerTask (triggered by the platform via AlarmReceiver to begin * polling the server 30 min ahead of the scheduled time of the arrival for which the reminder is * set), the NotifierTask (triggered by OBA Android when we should fire a notification for a * reminder), or the CancelNotifyTask (for when a notification is canceled). * * This Service is not constructed to continously run - instead, it can shut down in between the * execution of tasks. For example, the PollerTask actually reschedules itself each time it polls * (in PollerTask.poll1()), so the TripService service could shut down in between polling events. * * Following #290, mNotifications is only used as a semaphore to synchronize the multiple tasks and * shutdown of the Service. This is a complex implementation prone to multi-threading and * synchronization issues. We should examine a re-implementation of the reminder service - see * #493 * for details. */ public class TripService extends Service { public static final String TAG = "TripService"; /** * Actions - should match intent-filter actions for AlarmReceiver in AndroidManifest.xml * * NOTE: The action names in this class should not be changed. They need to stay under the * BuildConfig.APPLICATION_ID (for the original OBA brand, "com.joulespersecond.seattlebusbot") * namespace to support backwards compatibility with existing installed apps */ public static final String ACTION_SCHEDULE = BuildConfig.APPLICATION_ID + ".action.SCHEDULE"; public static final String ACTION_POLL = BuildConfig.APPLICATION_ID + ".action.POLL"; public static final String ACTION_NOTIFY = BuildConfig.APPLICATION_ID + ".action.NOTIFY"; public static final String ACTION_CANCEL = BuildConfig.APPLICATION_ID + ".action.CANCEL"; private static final String NOTIFY_TEXT = ".notifyText"; private ExecutorService mThreadPool; private NotificationManager mNM; /** * TODO - Remove mNotifications - it's now only used as a semaphore to synchronize the multiple * tasks and shutdown of the Service. However, this requires a new reminders impl (see #493). */ private ConcurrentHashMap<Integer, Notification> mNotifications; @Override public void onCreate() { mThreadPool = Executors.newSingleThreadExecutor(); mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mNotifications = new ConcurrentHashMap<Integer, Notification>(); } @Override public void onDestroy() { //Log.d(TAG, "service destroyed"); if (mThreadPool != null) { mThreadPool.shutdownNow(); // TODO: Await termination??? } } // // This is the old onStart method that will be called on the pre-2.0 // platform. On 2.0 or later we override onStartCommand so this // method will not be called. // @Override public void onStart(Intent intent, int startId) { handleCommand(intent, startId); } @Override public int onStartCommand(Intent intent, int flags, int startId) { return handleCommand(intent, startId); } private final class TaskContextImpl implements TaskContext { private final int mStartId; public TaskContextImpl(int startId) { mStartId = startId; } @Override public void taskComplete() { //Log.d(TAG, "Task complete: " + mStartId); // If we have notifications, then we can't stop ourselves. if (mNotifications.isEmpty()) { stopSelfResult(mStartId); } } @Override public void setNotification(int id, Notification notification) { mNotifications.put(id, notification); mNM.notify(id, notification); } @Override public void cancelNotification(int id) { mNM.cancel(id); mNotifications.remove(id); // If there are no more notifications // (and nothing else to process in the thread queue?) // stop the service. if (mNotifications.isEmpty()) { //Log.d(TAG, "Stopping service"); stopSelf(); } } } private int handleCommand(Intent intent, int startId) { final String action = intent.getAction(); final TaskContextImpl taskContext = new TaskContextImpl(startId); final Uri uri = intent.getData(); //Log.d(TAG, "Handle command: startId=" + startId + // " action=" + action + // " uri=" + uri); if (ACTION_SCHEDULE.equals(action)) { mThreadPool.submit(new SchedulerTask(this, taskContext, uri)); return START_REDELIVER_INTENT; } else if (ACTION_POLL.equals(action)) { mThreadPool.submit(new PollerTask(this, taskContext, uri)); return START_NOT_STICKY; } else if (ACTION_NOTIFY.equals(action)) { // Create the notification String notifyText = intent.getStringExtra(NOTIFY_TEXT); mThreadPool.submit(new NotifierTask(this, taskContext, uri, notifyText)); return START_REDELIVER_INTENT; } else if (ACTION_CANCEL.equals(action)) { mThreadPool.submit(new CancelNotifyTask(this, taskContext, uri)); return START_NOT_STICKY; } else { Log.e(TAG, "Unknown action: " + action); //stopSelfResult(startId); return START_NOT_STICKY; } } @Override public IBinder onBind(Intent arg0) { return mBinder; } private final IBinder mBinder = new Binder() { @Override protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { return super.onTransact(code, data, reply, flags); } }; // // Trip helpers // public static void scheduleAll(Context context) { final Intent intent = new Intent(context, TripService.class); intent.setAction(TripService.ACTION_SCHEDULE); intent.setData(ObaContract.Trips.CONTENT_URI); context.startService(intent); } public static void pollTrip(Context context, Uri alertUri, long triggerTime) { Intent intent = new Intent(TripService.ACTION_POLL, alertUri, context, AlarmReceiver.class); PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT); AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarm.set(AlarmManager.RTC_WAKEUP, triggerTime, alarmIntent); } public static void notifyTrip(Context context, Uri alertUri, String notifyText) { final Intent intent = new Intent(context, TripService.class); intent.setAction(ACTION_NOTIFY); intent.setData(alertUri); intent.putExtra(NOTIFY_TEXT, notifyText); context.startService(intent); } public static String getRouteShortName(Context context, String id) { return UIUtils.stringForQuery(context, Uri.withAppendedPath( ObaContract.Routes.CONTENT_URI, id), ObaContract.Routes.SHORTNAME ); } }