package com.jakehilborn.speedr;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.jakehilborn.speedr.utils.FormatTime;
import com.jakehilborn.speedr.utils.Prefs;
import com.jakehilborn.speedr.utils.UnitUtils;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Calendar;
import java.util.GregorianCalendar;
//MainService may run on the main thread shared by the UI. The only long running operations MainService does are network
//calls. These calls are all async so there is no blocking. Using Service instead of IntentService for simplicity sake.
public class MainService extends Service implements StatsCalculator.Callback {
private GoogleApiClient googleApiClient;
private static final int NOTIFICATION_ID = 1;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
private StatsCalculator statsCalculator;
private LimitFetcher limitFetcher;
public long stopTime;
//Binder for MainActivity to poll data from MainService
private final IBinder binder = new LocalBinder();
public class LocalBinder extends Binder {
MainService getService() {
return MainService.this;
}
}
public IBinder onBind(Intent intent) {
return binder;
}
//Callback for MainService to push data to MainActivity
private Callback callback;
private Handler handler = new Handler(Looper.getMainLooper());
public interface Callback {
void onUIDataUpdate(UIData uiData);
}
public void setCallback(Callback callback) {
this.callback = callback;
}
@Override
public void onCreate() {
super.onCreate();
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "onCreate()");
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notificationBuilder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_stat)
.setContentTitle(getString(R.string.notification_init_title))
.setContentText(getString(R.string.notification_init_text))
.setContentIntent(pendingIntent)
.setOngoing(true);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "onStartCommand()");
statsCalculator = new StatsCalculator();
statsCalculator.setCallback(this);
statsCalculator.setTimeSaved(Prefs.getSessionTimeSaved(this));
limitFetcher = new LimitFetcher(statsCalculator);
final LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(final Location location) {
handleLocationChange(location);
}
};
googleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
@Override
@SuppressWarnings("MissingPermission") //Location permission is granted before the MainService is started
public void onConnected(Bundle bundle) {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "Location connecting");
LocationRequest locationRequest = new LocationRequest();
locationRequest.setInterval(1000); //Request GPS location every 1 second
locationRequest.setFastestInterval(500);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
try {
LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener);
} catch (IllegalStateException e) {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "Play Services illegal state, retrying connection");
Crashlytics.logException(e);
googleApiClient.connect(); //Handling common error "GoogleApiClient is not connected yet"
}
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "Location connected");
}
@Override
public void onConnectionSuspended(int i) {
if (googleApiClient != null && googleApiClient.isConnected()) { //Remove pending updates, let GoogleAPIClient auto reconnect to restart updates
LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, locationListener).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(@NonNull Status status) {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "Location suspended");
}
});
}
}
})
.addApi(LocationServices.API)
.build();
googleApiClient.connect();
return START_STICKY;
}
private void handleLocationChange(Location location) {
statsCalculator.setLocation(location);
statsCalculator.calcTimeSaved();
if (statsCalculator.isLimitStale()) {
limitFetcher.fetchLimit(this, location.getLatitude(), location.getLongitude());
if (statsCalculator.isNetworkCheckStale()) {
checkNetworkDown();
}
}
UIData uiData = buildUIData();
updateMainActivity(uiData);
updateNotification(uiData);
}
@Override
public void handleNetworkUpdate() {
UIData uiData = buildUIData();
updateMainActivity(uiData);
updateNotification(uiData);
}
private UIData buildUIData() {
UIData uiData = new UIData();
if (Prefs.isUseKph(this)) {
uiData.setSpeed(UnitUtils.msToKph(statsCalculator.getSpeed()));
uiData.setLimit(UnitUtils.msToKphRoundToFive(statsCalculator.getLimit()));
} else { //mph
uiData.setSpeed(UnitUtils.msToMph(statsCalculator.getSpeed()));
uiData.setLimit(UnitUtils.msToMphRoundToFive(statsCalculator.getLimit()));
}
uiData.setTimeSaved(statsCalculator.getTimeSaved());
uiData.setFirstLimitTime(statsCalculator.getFirstLimitTime());
uiData.setNetworkDown(statsCalculator.isNetworkDown());
return uiData;
}
public UIData pollUIData() {
return buildUIData();
}
private void updateMainActivity(final UIData uiData) {
handler.post(new Runnable() {
public void run() {
if (callback != null) callback.onUIDataUpdate(uiData);
}
});
}
private void updateNotification(UIData uiData) {
String timeSaved = FormatTime.nanosToLongHand(this, uiData.getTimeSaved());
String speed = " "; //Padding to prevent values from shifting too much in notification
if (uiData.getSpeed() == null) {
speed = " 0";
} else {
if (uiData.getSpeed() >= 10) speed = " ";
if (uiData.getSpeed() >= 100) speed = ""; //Assumes currentSpeed won't exceed 999
speed += uiData.getSpeed();
}
String limit = " ";
if (uiData.getLimit() == null || uiData.getLimit() == 0) {
limit = " --";
} else {
if (uiData.getLimit() >= 10) limit = " ";
if (uiData.getLimit() >= 100) limit = ""; //Assumes currentLimit won't exceed 999
limit += uiData.getLimit();
}
notificationBuilder
.setContentTitle(getString(R.string.notification_time) + ": " + timeSaved)
.setContentText(getString(R.string.notification_speed_limit) + ": " + limit + " | " + getString(R.string.notification_speed) + ": " + speed);
if (notificationManager != null) notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
private void checkNetworkDown() {
new Thread() {
public void run() {
boolean isNetworkDown;
try {
HttpURLConnection conn = (HttpURLConnection) new URL("https://google.com").openConnection(); //ping
conn.setConnectTimeout(5000);
conn.connect();
isNetworkDown = conn.getResponseCode() != HttpURLConnection.HTTP_OK;
} catch (IOException e) {
isNetworkDown = true;
}
if (isNetworkDown) {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "network down");
} else {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "network up");
}
statsCalculator.setNetworkDown(isNetworkDown);
handleNetworkUpdate();
}
}.start();
}
private void persistTimeSaved(Double timeSaved, long firstLimitTime) {
long driveTime;
if (firstLimitTime == 0) {
driveTime = 0; //User stopped MainService before 1st speed limit was received
} else if (stopTime == 0) {
driveTime = System.nanoTime() - firstLimitTime; //Service shutting down despite user not clicking the stop button in MainActivity
} else {
driveTime = stopTime - firstLimitTime;
}
GregorianCalendar now = new GregorianCalendar();
Double sessionTimeSavedDelta = timeSaved - Prefs.getSessionTimeSaved(this);
Prefs.setSessionTimeSaved(this, timeSaved);
Prefs.setSessionDriveTime(this, driveTime + Prefs.getSessionDriveTime(this));
//redundant cast to int to suppress false positive IDE error
if (Prefs.getTimeSavedWeekNum(this) != (int) now.get(Calendar.WEEK_OF_YEAR)) {
Prefs.setTimeSavedWeekNum(this, now.get(Calendar.WEEK_OF_YEAR));
Prefs.setTimeSavedWeek(this, sessionTimeSavedDelta);
Prefs.setDriveTimeWeek(this, driveTime);
} else {
Prefs.setTimeSavedWeek(this, sessionTimeSavedDelta + Prefs.getTimeSavedWeek(this));
Prefs.setDriveTimeWeek(this, driveTime + Prefs.getDriveTimeWeek(this));
}
//Month is zero-indexed. Adding 1 since Prefs returns '0' as the default value if it has not yet been set
if (Prefs.getTimeSavedMonthNum(this) != (int) now.get(Calendar.MONTH) + 1) {
Prefs.setTimeSavedMonthNum(this, now.get(Calendar.MONTH) + 1);
Prefs.setTimeSavedMonth(this, sessionTimeSavedDelta);
Prefs.setDriveTimeMonth(this, driveTime);
} else {
Prefs.setTimeSavedMonth(this, sessionTimeSavedDelta + Prefs.getTimeSavedMonth(this));
Prefs.setDriveTimeMonth(this, driveTime + Prefs.getDriveTimeMonth(this));
}
if (Prefs.getTimeSavedYearNum(this) != (int) now.get(Calendar.YEAR)) {
Prefs.setTimeSavedYearNum(this, now.get(Calendar.YEAR));
Prefs.setTimeSavedYear(this, sessionTimeSavedDelta);
Prefs.setDriveTimeYear(this, driveTime);
} else {
Prefs.setTimeSavedYear(this, sessionTimeSavedDelta + Prefs.getTimeSavedYear(this));
Prefs.setDriveTimeYear(this, driveTime + Prefs.getDriveTimeYear(this));
}
}
@Override
public void onDestroy() {
Crashlytics.log(Log.INFO, MainService.class.getSimpleName(), "onDestroy()");
persistTimeSaved(statsCalculator.getTimeSaved(), statsCalculator.getFirstLimitTime());
notificationManager.cancel(NOTIFICATION_ID);
notificationManager = null;
limitFetcher.destroy(this);
if (googleApiClient != null) googleApiClient.disconnect();
super.onDestroy();
}
}