package net.maxbraun.mirror;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import net.maxbraun.mirror.Commute.CommuteSummary;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* A helper class to regularly retrieve commute time estimates.
*/
public class Commute extends DataUpdater<CommuteSummary> {
private static final String TAG = Commute.class.getSimpleName();
/**
* The time in milliseconds between API calls to update the commute time.
*/
private static final long UPDATE_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(1);
/**
* The time in milliseconds from now which is used as the future traffic reference point.
*/
private static final long FUTURE_DELTA_MILLIS = TimeUnit.MINUTES.toMillis(15);
/**
* The time delta in seconds below which the traffic trend is considered flat.
*/
private static final long TREND_THRESHOLD_SECONDS = TimeUnit.MINUTES.toSeconds(2);
/**
* The travel mode using standard driving directions using the road network.
*/
private static final String MODE_DRIVING = "driving";
/**
* The travel mode using walking directions via pedestrian paths and sidewalks.
*/
private static final String MODE_WALKING = "walking";
/**
* The travel mode using bicycling directions via bicycle paths and preferred streets.
*/
private static final String MODE_BICYCLING = "bicycling";
/**
* The travel mode using directions via public transit routes.
*/
private static final String MODE_TRANSIT = "transit";
/**
* The encoding used for request URLs.
*/
private static final String URL_ENCODE_FORMAT = "UTF-8";
/**
* The context used to load string resources.
*/
private final Context context;
/**
* A summary of current commute data.
*/
public static class CommuteSummary {
/**
* A human-readable text summary.
*/
public final String text;
/**
* The icon representing the travel mode.
*/
public final Drawable travelModeIcon;
/**
* The icon representing the traffic trend or {@code null} if it is flat.
*/
public final Drawable trafficTrendIcon;
public CommuteSummary(String text, Drawable travelModeIcon, Drawable trafficTrendIcon) {
this.text = text;
this.travelModeIcon = travelModeIcon;
this.trafficTrendIcon = trafficTrendIcon;
}
}
public Commute(Context context, UpdateListener<CommuteSummary> updateListener) {
super(updateListener, UPDATE_INTERVAL_MILLIS);
this.context = context;
}
@Override
protected CommuteSummary getData() {
// Get the latest data from the Google Maps Directions API for one departure now and one in the
// future, to compare traffic.
long nowMillis = System.currentTimeMillis();
long futureMillis = nowMillis + FUTURE_DELTA_MILLIS;
String nowRequestUrl = getRequestUrl(nowMillis);
String futureRequestUrl = getRequestUrl(futureMillis);
// Parse the data we are interested in from the response JSON.
try {
JSONObject nowResponse = Network.getJson(nowRequestUrl);
JSONObject futureResponse = Network.getJson(futureRequestUrl);
if ((nowResponse != null) && (futureResponse != null)) {
return parseCommuteSummary(nowResponse, futureResponse);
} else {
return null;
}
} catch (JSONException e) {
Log.e(TAG, "Failed to parse directions JSON.", e);
return null;
}
}
/**
* Creates the URL for a Google Maps Directions API request based on origin and destination
* addresses from resources.
*/
private String getRequestUrl(long departureTimeMillis) {
try {
return String.format(Locale.US, "https://maps.googleapis.com/maps/api/directions/json" +
"?origin=%s" +
"&destination=%s" +
"&mode=%s" +
"&departure_time=%d" +
"&key=%s",
URLEncoder.encode(context.getString(R.string.home), URL_ENCODE_FORMAT),
URLEncoder.encode(context.getString(R.string.work), URL_ENCODE_FORMAT),
context.getString(R.string.travel_mode),
departureTimeMillis / 1000,
context.getString(R.string.google_maps_directions_api_key));
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to create request URL.", e);
return null;
}
}
/**
* Reads the duration in traffic and route summary from the response. API documentation:
* https://developers.google.com/maps/documentation/directions/intro
*/
private CommuteSummary parseCommuteSummary(JSONObject nowResponse, JSONObject futureResponse)
throws JSONException {
String nowStatus = nowResponse.getString("status");
String futureStatus = futureResponse.getString("status");
if (!"OK".equals(nowStatus) || !"OK".equals(futureStatus)) {
Log.e(TAG, String.format("Error status in response: %s %s", nowStatus, futureStatus));
return null;
}
// Expect exactly one route.
JSONArray nowRoutes = nowResponse.getJSONArray("routes");
JSONObject nowRoute = nowRoutes.getJSONObject(0);
JSONArray futureRoutes = futureResponse.getJSONArray("routes");
JSONObject futureRoute = futureRoutes.getJSONObject(0);
// Expect exactly one leg.
JSONArray nowLegs = nowRoute.getJSONArray("legs");
JSONObject nowLeg = nowLegs.getJSONObject(0);
JSONArray futureLegs = futureRoute.getJSONArray("legs");
JSONObject futureLeg = futureLegs.getJSONObject(0);
// Get the duration now, with traffic if available.
JSONObject nowDuration;
boolean nowHasTraffic = nowLeg.has("duration_in_traffic");
if (nowHasTraffic) {
nowDuration = nowLeg.getJSONObject("duration_in_traffic");
} else {
nowDuration = nowLeg.getJSONObject("duration");
}
String nowDurationText = nowDuration.getString("text");
long nowDurationSeconds = nowDuration.getLong("value");
Log.d(TAG, String.format("Duration now: %s (%s s) %b", nowDurationText,
nowDurationSeconds, nowHasTraffic));
// Get the duration in the future, with traffic if available.
JSONObject futureDuration;
boolean futureHasTraffic = futureLeg.has("duration_in_traffic");
if (futureHasTraffic) {
futureDuration = futureLeg.getJSONObject("duration_in_traffic");
} else {
futureDuration = futureLeg.getJSONObject("duration");
}
String futureDurationText = futureDuration.getString("text");
long futureDurationSeconds = futureDuration.getLong("value");
Log.d(TAG, String.format("Duration future: %s (%d secs) %b", futureDurationText,
futureDurationSeconds, futureHasTraffic));
// Create the text summary.
String nowSummaryText = nowRoute.getString("summary");
Log.d(TAG, "Summary text: " + nowSummaryText);
String text;
if (!TextUtils.isEmpty(nowSummaryText)) {
text = String.format("%s via %s", nowDurationText, nowSummaryText);
} else {
text = nowDurationText;
}
// Pick the icon for the travel mode.
String travelMode = context.getString(R.string.travel_mode);
int travelModeIconResource;
if (MODE_DRIVING.equals(travelMode)) {
travelModeIconResource = R.drawable.driving;
} else if (MODE_TRANSIT.equals(travelMode)) {
travelModeIconResource = R.drawable.transit;
} else if (MODE_WALKING.equals(travelMode)) {
travelModeIconResource = R.drawable.walking;
} else if (MODE_BICYCLING.equals(travelMode)) {
travelModeIconResource = R.drawable.bicycling;
} else {
Log.e(TAG, "Unknown travel mode: " + travelMode);
return null;
}
Log.d(TAG, "Using travel mode: " + travelMode);
Drawable travelModeIcon = context.getDrawable(travelModeIconResource);
// Check if there is a significant trend and use the corresponding icon.
Drawable trafficTrendIcon;
long trendSeconds = futureDurationSeconds - nowDurationSeconds;
Log.d(TAG, String.format("Traffic trend: %d secs", trendSeconds));
if (Math.abs(trendSeconds) >= TREND_THRESHOLD_SECONDS) {
int trendIconResource = trendSeconds > 0 ? R.drawable.trend_up : R.drawable.trend_down;
trafficTrendIcon = context.getDrawable(trendIconResource);
} else {
trafficTrendIcon = null;
}
return new CommuteSummary(text, travelModeIcon, trafficTrendIcon);
}
@Override
protected String getTag() {
return TAG;
}
}