/**
* Copyright (C) 2016 Cambridge Systematics, Inc.
*
* 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.directions.realtime;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.directions.model.ItineraryDescription;
import org.onebusaway.android.directions.tasks.TripRequest;
import org.onebusaway.android.directions.util.OTPConstants;
import org.onebusaway.android.directions.util.TripRequestBuilder;
import org.opentripplanner.api.model.Itinerary;
import org.opentripplanner.api.model.Leg;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class RealtimeService extends IntentService {
private static final String TAG = "RealtimeService";
private static final String ITINERARY_DESC = ".ItineraryDesc";
private static final String ITINERARY_END_DATE = ".ItineraryEndDate";
public RealtimeService() {
super("RealtimeService");
}
/**
* Start realtime updates.
*
* @param source Activity from which updates are started
* @param bundle Bundle with selected itinerary/parameters
*/
public static void start(Activity source, Bundle bundle) {
SharedPreferences prefs = Application.getPrefs();
if (!prefs.getBoolean(OTPConstants.PREFERENCE_KEY_LIVE_UPDATES, true)) {
return;
}
bundle.putSerializable(OTPConstants.NOTIFICATION_TARGET, source.getClass());
Intent intent = new Intent(OTPConstants.INTENT_START_CHECKS);
intent.putExtras(bundle);
source.sendBroadcast(intent);
}
@Override
public void onHandleIntent(Intent intent) {
Bundle bundle = intent.getExtras();
if (intent.getAction().equals(OTPConstants.INTENT_START_CHECKS)) {
disableListenForTripUpdates();
if (!possibleReschedule(bundle)) {
Itinerary itinerary = getItinerary(bundle);
startRealtimeUpdates(bundle, itinerary);
}
} else if (intent.getAction().equals(OTPConstants.INTENT_CHECK_TRIP_TIME)) {
checkForItineraryChange(bundle);
}
RealtimeWakefulReceiver.completeWakefulIntent(intent);
}
// Depending on preferences / whether there is realtime info, start updates.
private void startRealtimeUpdates(Bundle params, Itinerary itinerary) {
Log.d(TAG, "Checking whether to start realtime updates.");
boolean realtimeLegsOnItineraries = false;
for (Leg leg : itinerary.legs) {
if (leg.realTime) {
realtimeLegsOnItineraries = true;
}
}
if (realtimeLegsOnItineraries) {
Log.d(TAG, "Starting realtime updates for itinerary");
// init alarm mgr
getAlarmManager().setInexactRepeating(AlarmManager.RTC, new Date().getTime(),
OTPConstants.DEFAULT_UPDATE_INTERVAL_TRIP_TIME, getAlarmIntent(params));
} else {
Log.d(TAG, "No realtime legs on itinerary");
}
}
// Reschedule the start of checks, if necessary
// Return true if rescheduled
private boolean possibleReschedule(Bundle bundle) {
// Delay if this trip doesn't start for at least an hour
Date start = new TripRequestBuilder(bundle).getDateTime();
Date queryStart = new Date(start.getTime() - OTPConstants.REALTIME_SERVICE_QUERY_WINDOW);
boolean reschedule = new Date().before(queryStart);
if (reschedule) {
Log.d(TAG, "Start service at " + queryStart);
Intent future = new Intent(OTPConstants.INTENT_START_CHECKS);
future.putExtras(bundle);
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
0, future, PendingIntent.FLAG_CANCEL_CURRENT);
getAlarmManager().set(AlarmManager.RTC_WAKEUP, queryStart.getTime(), pendingIntent);
}
return reschedule;
}
private void checkForItineraryChange(final Bundle bundle) {
TripRequestBuilder builder = TripRequestBuilder.initFromBundleSimple(bundle);
ItineraryDescription desc = getItineraryDescription(bundle);
Class target = getNotificationTarget(bundle);
if (target == null) {
disableListenForTripUpdates();
return;
}
checkForItineraryChange(target, builder, desc);
}
private void checkForItineraryChange(final Class<? extends Activity> source, final TripRequestBuilder builder, final ItineraryDescription itineraryDescription) {
Log.d(TAG, "Check for change");
TripRequest.Callback callback = new TripRequest.Callback() {
@Override
public void onTripRequestComplete(List<Itinerary> itineraries, String url) {
if (itineraries == null || itineraries.isEmpty()) {
onTripRequestFailure(-1, null);
return;
}
// Check each itinerary. Notify user if our *current* itinerary doesn't exist
// or has a lower rank.
for (int i = 0; i < itineraries.size(); i++) {
ItineraryDescription other = new ItineraryDescription(itineraries.get(i));
if (itineraryDescription.itineraryMatches(other)) {
long delay = itineraryDescription.getDelay(other);
Log.d(TAG, "Schedule deviation on itinerary: " + delay);
if (Math.abs(delay) > OTPConstants.REALTIME_SERVICE_DELAY_THRESHOLD) {
Log.d(TAG, "Notify due to large early/late schedule deviation.");
showNotification(itineraryDescription,
(delay > 0) ? R.string.trip_plan_delay
: R.string.trip_plan_early,
R.string.trip_plan_notification_new_plan_text,
source, builder.getBundle(), itineraries);
disableListenForTripUpdates();
return;
}
// Otherwise, we are still good.
Log.d(TAG, "Itinerary exists and no large schedule deviation.");
checkDisableDueToTimeout(itineraryDescription);
return;
}
}
Log.d(TAG, "Did not find a matching itinerary in new call - notify user that something has changed.");
showNotification(itineraryDescription,
R.string.trip_plan_notification_new_plan_title,
R.string.trip_plan_notification_new_plan_text, source,
builder.getBundle(), itineraries);
disableListenForTripUpdates();
}
@Override
public void onTripRequestFailure(int result, String url) {
Log.e(TAG, "Failure checking itineraries. Result=" + result + ", url=" + url);
disableListenForTripUpdates();
}
};
builder.setListener(callback);
try {
builder.execute();
} catch (Exception e) {
e.printStackTrace();
disableListenForTripUpdates();
}
}
private void showNotification(ItineraryDescription description, int title, int message,
Class<? extends Activity> notificationTarget,
Bundle params, List<Itinerary> itineraries) {
String titleText = getResources().getString(title);
String messageText = getResources().getString(message);
Intent openIntent = new Intent(getApplicationContext(), notificationTarget);
openIntent.putExtras(params);
openIntent.putExtra(OTPConstants.INTENT_SOURCE, OTPConstants.Source.NOTIFICATION);
openIntent.putExtra(OTPConstants.ITINERARIES, (ArrayList<Itinerary>) itineraries);
openIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent openPendingIntent = PendingIntent
.getActivity(getApplicationContext(),
0,
openIntent,
PendingIntent.FLAG_CANCEL_CURRENT);
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(getApplicationContext())
.setSmallIcon(R.drawable.ic_stat_notification)
.setContentTitle(titleText)
.setStyle(new NotificationCompat.BigTextStyle().bigText(messageText))
.setContentText(messageText)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setContentIntent(openPendingIntent);
NotificationManager notificationManager =
(NotificationManager) getApplicationContext()
.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = mBuilder.build();
notification.defaults = Notification.DEFAULT_ALL;
notification.flags |= Notification.FLAG_AUTO_CANCEL | Notification.FLAG_SHOW_LIGHTS;
Integer notificationId = description.getId();
notificationManager.notify(notificationId, notification);
}
// If the end time for this itinerary has passed, disable trip updates.
private void checkDisableDueToTimeout(ItineraryDescription itineraryDescription) {
if (itineraryDescription.isExpired()) {
Log.d(TAG, "End of trip has passed.");
disableListenForTripUpdates();
}
}
public void disableListenForTripUpdates() {
Log.d(TAG, "Disable trip updates.");
getAlarmManager().cancel(getAlarmIntent(null));
}
private AlarmManager getAlarmManager() {
return (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
}
private PendingIntent getAlarmIntent(Bundle bundle) {
Intent intent = new Intent(OTPConstants.INTENT_CHECK_TRIP_TIME);
if (bundle != null) {
Bundle extras = getSimplifiedBundle(bundle);
intent.putExtras(extras);
}
PendingIntent alarmIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
return alarmIntent;
}
private Itinerary getItinerary(Bundle bundle) {
ArrayList<Itinerary> itineraries = (ArrayList<Itinerary>) bundle
.getSerializable(OTPConstants.ITINERARIES);
int i = bundle.getInt(OTPConstants.SELECTED_ITINERARY);
return itineraries.get(i);
}
private ItineraryDescription getItineraryDescription(Bundle bundle) {
String ids[] = bundle.getStringArray(ITINERARY_DESC);
long date = bundle.getLong(ITINERARY_END_DATE);
return new ItineraryDescription(Arrays.asList(ids), new Date(date));
}
private Class getNotificationTarget(Bundle bundle) {
String name = bundle.getString(OTPConstants.NOTIFICATION_TARGET);
try {
return Class.forName(name);
} catch(ClassNotFoundException e) {
Log.e(TAG, "unable to find class for name " + name);
}
return null;
}
private Bundle getSimplifiedBundle(Bundle params) {
Itinerary itinerary = getItinerary(params);
ItineraryDescription desc = new ItineraryDescription(itinerary);
Bundle extras = new Bundle();
new TripRequestBuilder(params).copyIntoBundleSimple(extras);
List<String> idList = desc.getTripIds();
String[] ids = idList.toArray(new String[idList.size()]);
extras.putStringArray(ITINERARY_DESC, ids);
extras.putLong(ITINERARY_END_DATE, desc.getEndDate().getTime());
Class<? extends Activity> source = (Class<? extends Activity>)
params.getSerializable(OTPConstants.NOTIFICATION_TARGET);
extras.putString(OTPConstants.NOTIFICATION_TARGET, source.getName());
return extras;
}
}