/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Tiny Travel Tracker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
package com.rareventure.gps2;
import org.acra.ErrorReporter;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.LocationManager;
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.RemoteCallbackList;
import android.provider.Settings;
import android.util.Log;
import com.rareventure.gps2.GTG.Requirement;
import com.rareventure.gps2.IGpsTrailerService;
import com.rareventure.gps2.IGpsTrailerServiceCallback;
import com.rareventure.gps2.R;
import com.rareventure.android.AndroidPreferenceSet;
import com.rareventure.android.Util;
import com.rareventure.android.database.DbDatastoreAccessor;
import com.rareventure.gps2.GTG.GTGEvent;
import com.rareventure.gps2.GTG.GTGEventListener;
import com.rareventure.gps2.GTG.SetupState;
import com.rareventure.gps2.database.GpsLocationCache;
import com.rareventure.gps2.database.GpsLocationRow;
import com.rareventure.gps2.database.TAssert;
import com.rareventure.gps2.reviewer.SettingsActivity;
import com.rareventure.gps2.reviewer.wizard.WelcomePage;
public class GpsTrailerService extends Service {
private static final String TAG = "GpsTrailerService";
final RemoteCallbackList<IGpsTrailerServiceCallback> mCallbacks = new RemoteCallbackList<IGpsTrailerServiceCallback>();
/**
* The IRemoteInterface is defined through IDL
*/
private final IGpsTrailerService.Stub mBinder = new IGpsTrailerService.Stub() {
public void registerCallback(IGpsTrailerServiceCallback cb) {
if (cb != null)
mCallbacks.register(cb);
}
public void unregisterCallback(IGpsTrailerServiceCallback cb) {
if (cb != null)
mCallbacks.unregister(cb);
}
};
private Handler mHandler;
public class LocalBinder extends Binder {
// private static final String DESCRIPTOR = "com.rareventure.tapmusic.ITapServiceCallback";
public LocalBinder() {
// this.attachInterface(this, DESCRIPTOR);
}
GpsTrailerService getService() {
return GpsTrailerService.this;
}
}
private GpsTrailerManager gpsManager;
private BroadcastReceiver batteryReceiver;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
/* ttt_installer:remove_line */Log.d(GTG.TAG, "Starting on intent " + intent);
//if we were already running and had a gpsManager, notify that we woke up
if (gpsManager != null)
gpsManager.notifyWoken();
mHandler = new Handler();
GTG.service = this;
//we want our service to restart if stopped because of memory constraints or whatever else
return Service.START_STICKY;
}
public GTGEventListener gtgEventListener = new GTGEventListener() {
@Override
public boolean onGTGEvent(GTGEvent event) {
if (event == GTGEvent.ERROR_GPS_DISABLED) {
updateNotification(FLAG_GPS_ENABLED, false);
stopSelf();
}
if (event == GTGEvent.ERROR_SDCARD_NOT_MOUNTED) {
updateNotification(FLAG_ERROR_SDCARD_NOT_MOUNTED, true);
stopSelf();
}
else if (event == GTGEvent.ERROR_LOW_FREE_SPACE) {
updateNotification(FLAG_ERROR_LOW_FREE_SPACE, true);
stopSelf();
}
else if (event == GTGEvent.ERROR_SERVICE_INTERNAL_ERROR) {
updateNotification(FLAG_ERROR_INTERNAL, true);
stopSelf();
}
else if (event == GTGEvent.ERROR_LOW_BATTERY) {
updateNotification(FLAG_BATTERY_LOW, true);
stopSelf();
}
else if (event == GTGEvent.TRIAL_PERIOD_EXPIRED) {
updateNotification(FLAG_TRIAL_EXPIRED, true);
stopSelf();
}
else if (event == GTGEvent.DOING_RESTORE) {
updateNotification(FLAG_IN_RESTORE, true);
stopSelf();
}
else if (event == GTGEvent.ERROR_UNLICENSED) {
//co: we initially don't want to do anything if the user is unlicensed,
// just find out how much piracy is a problem
// we don't show a notification if we quit due to the app being unlicensed
// stopSelf();
}
return false;
}
@Override
public void offGTGEvent(GTGEvent event) {
if (event == GTGEvent.ERROR_GPS_DISABLED) {
updateNotification(FLAG_GPS_ENABLED, true);
}
}
};
private static final int FLAG_GPS_ENABLED = 1;
private static final int FLAG_BATTERY_LOW = 2;
private static final int FLAG_FINISHED_STARTUP = 4;
private static final int FLAG_ERROR_INTERNAL = 8;
private static final int FLAG_ERROR_SDCARD_NOT_MOUNTED = 16;
private static final int FLAG_ERROR_LOW_FREE_SPACE = 32;
private static final int FLAG_COLLECT_ENABLED = 64;
private static final int FLAG_ERROR_DB_PROBLEM = 128;
private static final int FLAG_TRIAL_EXPIRED = 256;
private static final int FLAG_IN_RESTORE = 512;
private int notificationFlags = 0;
private static class NotificationSetting {
int iconId;
int msgId;
boolean sticky;
int onFlags, offFlags;
Intent intent;
boolean isOngoing;
public NotificationSetting(int iconId, int msgId, boolean sticky,
int onFlags, int offFlags, Intent intent, boolean isOngoing) {
super();
this.iconId = iconId;
this.msgId = msgId;
this.sticky = sticky;
this.onFlags = onFlags;
this.offFlags = offFlags;
this.intent = intent;
this.isOngoing = isOngoing;
}
/**
* @return true if all of the this.onFlags are on and all of the this.offFlags are off
*/
public boolean matches(int notificationFlags) {
return ((onFlags & notificationFlags) == onFlags)
&& ((Integer.MAX_VALUE - offFlags) | notificationFlags) == (Integer.MAX_VALUE - offFlags);
}
@Override
public String toString() {
return "NotificationSetting [iconId=" + iconId + ", msgId=" + msgId
+ ", sticky=" + sticky + ", onFlags=" + onFlags
+ ", offFlags=" + offFlags + "]";
}
}
/**
* Note, ordered by priority
*/
public static NotificationSetting[] NOTIFICATIONS = new NotificationSetting[] {
//note, in the following, if FLAG_COLLECT_ENABLED is *off*, we
//shut off the notification icon
new NotificationSetting(-1, -1, false, 0, FLAG_COLLECT_ENABLED,
null, false),
new NotificationSetting(-1, -1, false, FLAG_IN_RESTORE, 0,
null, false),
new NotificationSetting(-1, -1, false, FLAG_TRIAL_EXPIRED, 0, null,
false),
new NotificationSetting(R.drawable.red_error,
R.string.service_error_internal_error, true,
FLAG_ERROR_INTERNAL, 0, null, false),
new NotificationSetting(R.drawable.red_error,
R.string.service_error_sdcard_not_mounted, true,
FLAG_ERROR_SDCARD_NOT_MOUNTED, 0, null, false),
new NotificationSetting(R.drawable.red_error,
R.string.service_error_db_problem, true,
FLAG_ERROR_DB_PROBLEM, 0, null, false),
new NotificationSetting(R.drawable.red_error,
R.string.service_error_low_free_space, true,
FLAG_ERROR_LOW_FREE_SPACE, 0, null, false),
new NotificationSetting(-1, -1, false, FLAG_BATTERY_LOW, 0, null,
false),
new NotificationSetting(R.drawable.red_error,
R.string.service_gps_not_enabled, true, 0,
FLAG_GPS_ENABLED, new Intent(
Settings.ACTION_LOCATION_SOURCE_SETTINGS)
, false),
new NotificationSetting(R.drawable.green, R.string.service_active,
false, FLAG_GPS_ENABLED | FLAG_FINISHED_STARTUP
| FLAG_COLLECT_ENABLED, 0, null, true) };
private NotificationSetting currentNotificationSetting = null;
/**
* Updates the notification based on the current status of the system
*/
private void updateNotification(int flags, boolean isOn) {
if (isOn)
notificationFlags |= flags;
else
notificationFlags &= (Integer.MAX_VALUE ^ flags);
NotificationSetting oldNotSetting = currentNotificationSetting;
//choose first matching notification setting
for (NotificationSetting ns : NOTIFICATIONS) {
if (ns.matches(notificationFlags)) {
currentNotificationSetting = ns;
break;
}
}
/* ttt_installer:remove_line */Log.d(GTG.TAG, "Flags is " + notificationFlags + ", current notset is " + currentNotificationSetting);
if (currentNotificationSetting != oldNotSetting) {
showCurrentNotification();
}
}
@Override
public void onCreate() {
Log.d(TAG, "GPS Service Startup");
GTG.addGTGEventListener(gtgEventListener);
//reset all flags
updateNotification(Integer.MAX_VALUE, false);
updateNotification(FLAG_GPS_ENABLED, true);
//read isCollectData first from shared prefs and if its false, quit immediately
//we don't want to notify the user that we ran for any reason if this is false
// (for example the db was corrupted)
GTG.prefSet.loadAndroidPreferencesFromSharedPrefs(this);
//co: we initially don't want to do anything if the user is unlicensed,
// just find out how much piracy is a problem
if (!GTG.prefs.isCollectData ) //|| GTGEvent.ERROR_UNLICENSED.isOn)
{
turnOffNotification();
stopSelf();
return;
}
GTG.initRwtm.registerWritingThread();
try {
try {
GTG.requireInitialSetup(this, true);
GTG.requirePrefsLoaded(this);
if(!GTG.requireNotInRestore())
{
updateNotification(FLAG_IN_RESTORE, true);
stopSelf();
return;
}
Intent i = GTG.requireNotTrialWhenPremiumIsAvailable(this);
if (i != null) {
turnOffNotification();
stopSelf();
return;
}
if (!GTG.requireNotTrialExpired()) {
updateNotification(FLAG_TRIAL_EXPIRED, true);
stopSelf();
return;
}
if (!GTG.requireSdcardPresent(this)) {
updateNotification(FLAG_ERROR_SDCARD_NOT_MOUNTED, true);
stopSelf();
return;
}
if (!GTG.requireSystemInstalled(this)) {
stopSelf();
return;
}
int status = GTG.requireDbReady();
if (GTG.requireDbReady() != GTG.REQUIRE_DB_READY_OK) {
updateNotification(FLAG_ERROR_DB_PROBLEM, true);
stopSelf();
return;
}
GTG.requireEncrypt();
}
finally {
GTG.initRwtm.unregisterWritingThread();
}
updateNotification(FLAG_COLLECT_ENABLED, true);
//co because we don't want gps2data files in the root directory of sdcard. they take up too much space
// SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
// File dataFile = new File(root, "gps2data"+sdf.format(new Date())+".txt");
// gpsManager = new GpsTrailerManager(dataFile, this, TAG, this.getMainLooper());
gpsManager = new GpsTrailerManager(null, this, TAG,
this.getMainLooper());
//note that there is no way to receive the current status of the battery
//However, the battery receiver will get a notification as soon as its registered
// to the current status of the battery
batteryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
float batteryLeft = ((float) intent.getIntExtra(
BatteryManager.EXTRA_LEVEL, -1))
/ intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
/* ttt_installer:remove_line */Log.d(GTG.TAG, "Received battery info, batteryLeft: "+ batteryLeft);
int status = intent.getIntExtra("status",
BatteryManager.BATTERY_STATUS_UNKNOWN);
if (batteryLeft < GTG.prefs.minBatteryPerc
&& (status == BatteryManager.BATTERY_STATUS_DISCHARGING || status == BatteryManager.BATTERY_STATUS_UNKNOWN)) {
updateNotification(FLAG_BATTERY_LOW, true);
GTG.alert(GTGEvent.ERROR_LOW_BATTERY);
}
else
updateNotification(FLAG_BATTERY_LOW, false);
//we wait until now to finish startup
//because otherwise if the battery is low
//and we are awoken for some other event, we would otherwise show our icon for
//a brief period before we get the battery is low event
updateNotification(FLAG_FINISHED_STARTUP, true);
//just a hack, a reasonable place to check if the trial version has expired
checkTrialExpired();
}
};
IntentFilter batteryLevelFilter = new IntentFilter(
Intent.ACTION_BATTERY_CHANGED);
registerReceiver(batteryReceiver, batteryLevelFilter);
gpsManager.start();
}
catch (Exception e) {
shutdownWithException(e);
ErrorReporter.getInstance().handleException(e);
}
}
private boolean checkTrialExpired() {
if (GTG.calcDaysBeforeTrialExpired() == 0) {
Requirement.NOT_TRIAL_EXPIRED.reset();
//this will stop self
GTG.alert(GTGEvent.TRIAL_PERIOD_EXPIRED);
return true;
}
return false;
}
private void shutdownWithException(Exception e) {
Log.e(GTG.TAG, "Exception running gps service", e);
updateNotification(FLAG_ERROR_INTERNAL, true);
stopSelf();
}
/**
* Show a notification while this service is running.
* @param intent
*/
private void showCurrentNotification() {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
if (currentNotificationSetting.msgId == -1
&& currentNotificationSetting.iconId == -1) {
nm.cancel(GTG.FROG_NOTIFICATION_ID);
return;
}
CharSequence text = getText(currentNotificationSetting.msgId);
// Set the icon, scrolling text and timestamp
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(currentNotificationSetting.iconId);
builder.setOngoing(currentNotificationSetting.isOngoing);
builder.setAutoCancel(!currentNotificationSetting.isOngoing);
builder.setTicker(text);
// The PendingIntent to launch our activity if the user selects this notification
// TODO 2.5 make settings lite for notification bar only. Set it's task affinity
// different from the main app so hitting back doesn't cause "enter password" to be asked
Intent intent = new Intent(this, SettingsActivity.class);
PendingIntent contentIntent = PendingIntent
.getActivity(
this,
0,
currentNotificationSetting.intent != null ? currentNotificationSetting.intent
: intent,
PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(contentIntent);
Notification notification = builder.getNotification();
nm.notify(GTG.FROG_NOTIFICATION_ID, notification);
}
@Override
public void onDestroy() {
Log.d(TAG, "GPS Service Shutdown");
GTG.removeGTGEventListener(gtgEventListener);
if (currentNotificationSetting != null
&& !currentNotificationSetting.sticky) {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(GTG.FROG_NOTIFICATION_ID);
}
if (gpsManager != null)
gpsManager.shutdown();
if (batteryReceiver != null)
unregisterReceiver(batteryReceiver);
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public void turnOffNotification()
{
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(GTG.FROG_NOTIFICATION_ID);
}
public void shutdown() {
Util.runOnHandlerSynchronously(mHandler, new Runnable() {
@Override
public void run() {
stopSelf();
}
});
}
}