package org.witness.informacam.informa;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException;
import org.witness.informacam.Debug;
import org.witness.informacam.InformaCam;
import org.witness.informacam.R;
import org.witness.informacam.informa.suckers.AccelerometerSucker;
import org.witness.informacam.informa.suckers.EnvironmentalSucker;
import org.witness.informacam.informa.suckers.GeoFusedSucker;
import org.witness.informacam.informa.suckers.GeoHiResSucker;
import org.witness.informacam.informa.suckers.GeoSucker;
import org.witness.informacam.informa.suckers.PhoneSucker;
import org.witness.informacam.intake.Intake;
import org.witness.informacam.json.JSONArray;
import org.witness.informacam.json.JSONException;
import org.witness.informacam.json.JSONObject;
import org.witness.informacam.models.j3m.ILocation;
import org.witness.informacam.models.j3m.ILogPack;
import org.witness.informacam.models.j3m.ISuckerCache;
import org.witness.informacam.models.j3m.IDCIMDescriptor.IDCIMSerializable;
import org.witness.informacam.models.media.IMedia;
import org.witness.informacam.models.media.IRegion;
import org.witness.informacam.ui.AlwaysOnActivity;
import org.witness.informacam.utils.Constants.Actions;
import org.witness.informacam.utils.Constants.App;
import org.witness.informacam.utils.Constants.App.Informa;
import org.witness.informacam.utils.Constants.Codes;
import org.witness.informacam.utils.Constants.IManifest;
import org.witness.informacam.utils.Constants.Logger;
import org.witness.informacam.utils.Constants.SuckerCacheListener;
import org.witness.informacam.utils.Constants.Suckers;
import org.witness.informacam.utils.Constants.Suckers.CaptureEvent;
import org.witness.informacam.utils.Constants.Suckers.Phone;
import org.witness.informacam.utils.MediaHasher;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class InformaService extends Service implements SuckerCacheListener {
private final IBinder binder = new LocalBinder();
private long startTime = 0L;
private long realStartTime = 0L;
private int GPS_WAITING = 0;
private SensorLogger<GeoSucker> _geo;
private SensorLogger<PhoneSucker> _phone;
private SensorLogger<AccelerometerSucker> _acc;
private SensorLogger<EnvironmentalSucker> _env;
private boolean suckersActive = false;
private info.guardianproject.iocipher.File cacheFile, cacheRoot;
private List<String> cacheFiles = new ArrayList<String>();
private LoadingCache<Long, ILogPack> cache = null;
private final static long CACHE_MAX = 500;
private Timer cacheTimer;
InformaCam informaCam;
Handler h = new Handler();
String associatedMedia = null;
Intent stopIntent = new Intent().setAction(Actions.INFORMA_STOP);
private static InformaService mInstance = null;
public final static String ACTION_START_SUCKERS = "startsuckers";
public final static String ACTION_STOP_SUCKERS = "stopsuckers";
public final static String ACTION_RESET_CACHE = "resetcache";
private InformaBroadcaster[] broadcasters = {
new InformaBroadcaster(new IntentFilter(BluetoothDevice.ACTION_FOUND)),
new InformaBroadcaster(new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
};
private final static String LOG = App.Informa.LOG;
public class LocalBinder extends Binder {
public InformaService getService() {
return InformaService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public static InformaService getInstance ()
{
return mInstance;
}
@Override
public void onCreate() {
Log.d(LOG, "started.");
if (Debug.WAIT_FOR_DEBUGGER)
android.os.Debug.waitForDebugger();
mInstance = this;
informaCam = (InformaCam)getApplication();
if (informaCam.ioService == null || (!informaCam.ioService.isMounted()))
{
//this seems like an auto-restart; we should stop
stopSelf();
return;
}
cacheRoot = new info.guardianproject.iocipher.File(IManifest.CACHES);
if(!cacheRoot.exists()) {
cacheRoot.mkdir();
}
sendBroadcast(new Intent()
.putExtra(Codes.Keys.SERVICE, Codes.Routes.INFORMA_SERVICE)
.setAction(Actions.ASSOCIATE_SERVICE)
.putExtra(Codes.Extras.RESTRICT_TO_PROCESS, android.os.Process.myPid()));
init();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.getAction()!=null)
{
if (intent.getAction().equals(ACTION_START_SUCKERS))
{
this.startAllSuckers();
}
else if (intent.getAction().equals(ACTION_STOP_SUCKERS))
{
this.stopAllSuckers();
}
else if (intent.getAction().equals(ACTION_RESET_CACHE))
{
resetCacheFiles();
}
}
return START_STICKY;//super.onStartCommand(intent, flags, startId);
}
/**
* Gets the state of Airplane Mode.
*
* @param context
* @return true if enabled.
*/
private static boolean isAirplaneModeOn(Context context) {
return Settings.System.getInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
public ILocation getCurrentLocation() {
if (_geo != null)
{
double[] dLoc = ((GeoSucker) _geo).updateLocation();
if (dLoc != null)
return new ILocation(dLoc);
}
return null;
}
public long getCurrentTime() {
//return System.currentTimeMillis() + (realStartTime == 0 ? 0 : (startTime - realStartTime));
return System.currentTimeMillis();
}
public long getTimeOffset() {
return realStartTime == 0 ? 0 : (startTime - realStartTime);
}
public void associateMedia(IMedia media) {
this.associatedMedia = media._id;
}
public void unassociateMedia() {
this.associatedMedia = null;
}
private void init() {
h.post(new Runnable() {
@Override
public void run() {
startTime = System.currentTimeMillis();
long currentTime = 0;
if (_geo != null)
{
currentTime = ((GeoSucker) _geo).getTime();
if(currentTime != 0) {
realStartTime = currentTime;
}
double[] currentLocation = ((GeoSucker) _geo).updateLocation();
if(currentTime == 0 || currentLocation == null) {
GPS_WAITING++;
if(GPS_WAITING < Suckers.GPS_WAIT_MAX) {
h.postDelayed(this, 200);
return;
} else {
//Don't show notifications the user can't do anything about
//Toast.makeText(InformaService.this, getString(R.string.gps_not_available_your), Toast.LENGTH_LONG).show();
GPS_WAITING = 0; //reset
}
}
onUpdate(((GeoSucker) _geo).forceReturn());
}
if (_phone != null)
{
onUpdate(((PhoneSucker) _phone).forceReturn());
}
if (informaCam != null)
{
sendBroadcast(new Intent()
.setAction(Actions.INFORMA_START)
.putExtra(Codes.Extras.RESTRICT_TO_PROCESS, informaCam.getProcess()));
}
}
});
}
public void flushCache() {
flushCache(null);
}
public void flushCache(IMedia m) {
saveCache(true, m);
}
private void initCache() {
try {
cacheFile = new info.guardianproject.iocipher.File(cacheRoot, MediaHasher.hash(new String(startTime + "_" + System.currentTimeMillis()).getBytes(), "MD5"));
cacheFiles = new ArrayList<String>();
cacheFiles.add(cacheFile.getAbsolutePath());
} catch (NoSuchAlgorithmException e) {
Logger.e(LOG, e);
} catch (IOException e) {
Logger.e(LOG, e);
}
cacheTimer = new Timer();
cacheTimer.schedule(new TimerTask() {
@Override
public void run() {
if(cache != null) {
if(cache.size() >= CACHE_MAX) {
saveCache(true, null);
}
}
}
}, 0, 4000L);
startTime = System.currentTimeMillis();
cache = CacheBuilder.newBuilder()
.build(new CacheLoader<Long, ILogPack>() {
@Override
public ILogPack load(Long timestamp) throws Exception {
return cache.getUnchecked(timestamp);
}
});
}
private void saveCache() {
saveCache(false, null);
}
private Thread mThread = null;
private void saveCache(final boolean restartCache, final IMedia m) {
if (mThread == null || (!mThread.isAlive()))
{
if (cache == null) //service may have been stopped
return;
Log.d(LOG, "CACHE SIZE SO FAR: " + cache.size() + "\nSaving and restarting cache...");
mThread = new Thread(new Runnable() {
@Override
public void run() {
ISuckerCache suckerCache = new ISuckerCache();
JSONArray cacheArray = new JSONArray();
Iterator<Entry<Long, ILogPack>> cIt = cache.asMap().entrySet().iterator();
while(cIt.hasNext()) {
JSONObject cacheMap = new JSONObject();
Entry<Long, ILogPack> c = cIt.next();
try {
cacheMap.put(String.valueOf(c.getKey()), c.getValue());
cacheArray.put(cacheMap);
} catch(JSONException e) {
Logger.e(LOG, e);
}
}
suckerCache.timeOffset = realStartTime;
suckerCache.cache = cacheArray;
// TODO: XXX: collision errors (ConcurrentModificationException)
informaCam.ioService.saveBlob(suckerCache.asJson().toString().getBytes(), cacheFile);
if(associatedMedia != null) {
IMedia media = informaCam.mediaManifest.getById(associatedMedia);
if(media.associatedCaches == null) {
media.associatedCaches = new ArrayList<String>();
}
if(!media.associatedCaches.contains(cacheFile.getAbsolutePath())) {
media.associatedCaches.add(cacheFile.getAbsolutePath());
}
try
{
// Logger.d(LOG, "OK-- I am about to save the cache reference. is this still correct?\n" + media.asJson().toString());
media.save();
}
catch (Exception e)
{
Logger.e(LOG, e);
return;
}
}
if(m != null) {
associateMedia(m);
}
InformaService.this.onCacheSaved(restartCache);
}
});
mThread.start();
}
else
{
Log.d(LOG, "CACHE SAVE IN PROGRESS... WAITING IN LINE ...");
}
}
public boolean suckersActive ()
{
return suckersActive;
}
private void startAllSuckers() {
if(suckersActive) {
return;
}
Logger.d(LOG, "STARTING INFORMA SUCKERS...");
for(BroadcastReceiver broadcaster : broadcasters) {
this.registerReceiver(broadcaster, ((InformaBroadcaster) broadcaster).intentFilter);
}
initCache();
boolean prefGpsEnableHires = PreferenceManager.getDefaultSharedPreferences(this.getApplicationContext()).getBoolean("prefGpsEnableHires",false);
boolean hasPlayServices = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getApplicationContext()) == ConnectionResult.SUCCESS;
if (prefGpsEnableHires || (!hasPlayServices))
_geo = new GeoHiResSucker(this);
else
_geo = new GeoFusedSucker(this);
_geo.setSuckerCacheListener(this);
_phone = new PhoneSucker(this);
_phone.setSuckerCacheListener(this);
_acc = new AccelerometerSucker(this);
_acc.setSuckerCacheListener(this);
_env = new EnvironmentalSucker(this);
_env.setSuckerCacheListener(this);
try {
double[] dLoc = ((GeoSucker) _geo).updateLocation();
if (dLoc != null)
((EnvironmentalSucker)_env).updateSeaLevelPressure(dLoc[0], dLoc[1]);
} catch (Exception e) {
Log.d(Informa.LOG,"error updating sea level pressure",e);
}
suckersActive = true;
showNotification();
}
private void showNotification ()
{
Intent notificationIntent = new Intent(this, AlwaysOnActivity.class);
PendingIntent pendingIntent=PendingIntent.getActivity(this, 0,
notificationIntent, Intent.FLAG_ACTIVITY_NEW_TASK);
Notification notification=new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_action_camera)
.setContentTitle(getString(R.string.proof_mode_activated))
.setContentIntent(pendingIntent)
.setOngoing(true).build();
startForeground(991199, notification);
}
private void stopAllSuckers() {
if(!suckersActive) {
return;
}
Logger.d(LOG, "STOPPING INFORMA SUCKERS...");
saveCache();
if (_phone != null)
_phone.getSucker().stopUpdates();
if (_acc != null)
_acc.getSucker().stopUpdates();
if (_geo != null)
_geo.getSucker().stopUpdates();
if (_env != null)
_env.getSucker().stopUpdates();
_geo = null;
_phone = null;
_acc = null;
_env = null;
for(BroadcastReceiver b : broadcasters) {
try
{
unregisterReceiver(b);
}
catch (IllegalArgumentException iae)
{
//some broadcasters may not be registered; don't let this stop us from getting destroyed!
}
}
suckersActive = false;
stopForeground(true);
}
@Override
public void onDestroy() {
super.onDestroy();
stopAllSuckers();
sendBroadcast(stopIntent.putExtra(Codes.Extras.RESTRICT_TO_PROCESS, informaCam.getProcess()));
sendBroadcast(new Intent()
.putExtra(Codes.Keys.SERVICE, Codes.Routes.INFORMA_SERVICE)
.setAction(Actions.DISASSOCIATE_SERVICE)
.putExtra(Codes.Extras.RESTRICT_TO_PROCESS, android.os.Process.myPid()));
}
public List<ILogPack> getAllEventsByType(final int type) throws InterruptedException, ExecutionException, JSONException {
Iterator<Entry<Long, ILogPack>> cIt = cache.asMap().entrySet().iterator();
List<ILogPack> events = new ArrayList<ILogPack>();
while(cIt.hasNext()) {
Entry<Long, ILogPack> entry = cIt.next();
if(entry.getValue().has(CaptureEvent.Keys.TYPE) && entry.getValue().getInt(CaptureEvent.Keys.TYPE) == type)
events.add(entry.getValue());
}
return events;
}
public List<Entry<Long, ILogPack>> getAllEventsByTypeWithTimestamp(final int type) throws JSONException, InterruptedException, ExecutionException {
Iterator<Entry<Long, ILogPack>> cIt = cache.asMap().entrySet().iterator();
List<Entry<Long, ILogPack>> events = new ArrayList<Entry<Long, ILogPack>>();
while(cIt.hasNext()) {
Entry<Long, ILogPack> entry = cIt.next();
if(entry.getValue().has(CaptureEvent.Keys.TYPE) && entry.getValue().getInt(CaptureEvent.Keys.TYPE) == type)
events.add(entry);
}
return events;
}
public Entry<Long, ILogPack> getEventByTypeWithTimestamp(final int type) throws JSONException, InterruptedException, ExecutionException {
Iterator<Entry<Long, ILogPack>> cIt = cache.asMap().entrySet().iterator();
Entry<Long, ILogPack> entry = null;
while(cIt.hasNext() && entry == null) {
Entry<Long, ILogPack> e = cIt.next();
if(e.getValue().has(CaptureEvent.Keys.TYPE) && e.getValue().getInt(CaptureEvent.Keys.TYPE) == type)
entry = e;
}
return entry;
}
public ILogPack getEventByType(final int type) throws JSONException, InterruptedException, ExecutionException {
Iterator<ILogPack> cIt = cache.asMap().values().iterator();
ILogPack ILogPack = null;
while(cIt.hasNext() && ILogPack == null) {
ILogPack lp = cIt.next();
if(lp.has(CaptureEvent.Keys.TYPE) && lp.getInt(CaptureEvent.Keys.TYPE) == type)
ILogPack = lp;
}
return ILogPack;
}
@SuppressWarnings("unchecked")
public boolean removeRegion(IRegion region) {
try {
ILogPack ILogPack = cache.getIfPresent(region.timestamp);
if(ILogPack.has(CaptureEvent.Keys.TYPE) && ILogPack.getInt(CaptureEvent.Keys.TYPE) == CaptureEvent.REGION_GENERATED) {
ILogPack.remove(CaptureEvent.Keys.TYPE);
}
Iterator<String> repIt = region.asJson().keys();
while(repIt.hasNext()) {
ILogPack.remove(repIt.next());
}
return true;
} catch(NullPointerException e) {
Log.e(LOG, e.toString());
e.printStackTrace();
} catch (Exception e) {
Log.e(LOG, e.toString(),e);
}
return false;
}
@SuppressWarnings("unchecked")
public void addRegion(IRegion region) {
ILogPack ILogPack = new ILogPack(CaptureEvent.Keys.TYPE, CaptureEvent.REGION_GENERATED, true);
if (_geo != null)
{
ILogPack regionLocationData = ((GeoSucker) _geo).forceReturn();
try {
ILogPack.put(CaptureEvent.Keys.REGION_LOCATION_DATA, regionLocationData);
} catch (Exception e) {
Log.e(LOG, e.toString(),e);
}
}
Iterator<String> rIt = region.asJson().keys();
while(rIt.hasNext()) {
String key = rIt.next();
try {
ILogPack.put(key, region.asJson().get(key));
} catch (Exception e) {
Log.e(LOG, e.toString(),e);
}
}
//Log.d(LOG, "HEY NEW REGION: " + ILogPack.asJson().toString());
region.timestamp = onUpdate(ILogPack);
}
@SuppressWarnings("unchecked")
public void updateRegion(IRegion region) {
try {
ILogPack ILogPack = cache.getIfPresent(region.timestamp);
Iterator<String> repIt = region.asJson().keys();
while(repIt.hasNext()) {
String key = repIt.next();
ILogPack.put(key, region.asJson().get(key));
}
} catch(JSONException e) {
Log.e(LOG, e.toString(),e);
} catch(NullPointerException e) {
Log.e(LOG, "CONSIDERED HANDLED:\n" + e.toString(),e);
addRegion(region);
}
}
@SuppressWarnings({ "unchecked", "unused" })
private ILogPack JSONObjectToILogPack(JSONObject json) throws JSONException {
ILogPack ILogPack = new ILogPack();
Iterator<String> jIt = json.keys();
while(jIt.hasNext()) {
String key = jIt.next();
ILogPack.put(key, json.get(key));
}
return ILogPack;
}
@SuppressWarnings("unused")
private void pushToSucker(SensorLogger<?> sucker, ILogPack ILogPack) throws JSONException {
if(sucker.getClass().equals(PhoneSucker.class))
_phone.sendToBuffer(ILogPack);
}
public String getCacheFile() {
return cacheFile.getAbsolutePath();
}
public List<String> getCacheFiles() {
return cacheFiles;
}
private void resetCacheFiles ()
{
if (cacheFiles.size() > 0)
{
String lastCache = cacheFiles.get(cacheFiles.size()-1);
cacheFiles = new ArrayList<String>();
cacheFiles.add(lastCache);
}
}
@SuppressWarnings("unchecked")
@Override
public void onUpdate(long timestamp, ILogPack iLogPack) {
try {
if (cache == null)
initCache();
ILogPack lp = cache.getIfPresent(timestamp);
if(lp != null) {
synchronized(iLogPack) //lock access to lp so it is not modified
{
Iterator<String> lIt = lp.keys();
while(lIt.hasNext()) {
String key = lIt.next();
iLogPack.put(key, lp.get(key));
}
}
}
cache.put(timestamp, iLogPack);
} catch(JSONException e) {}
}
@Override
public long onUpdate(ILogPack ILogPack) {
long timestamp = getCurrentTime();
onUpdate(timestamp, ILogPack);
return timestamp;
}
class InformaBroadcaster extends BroadcastReceiver {
IntentFilter intentFilter;
public InformaBroadcaster(IntentFilter intentFilter) {
this.intentFilter = intentFilter;
}
@Override
public void onReceive(Context context, Intent intent) {
if (_phone != null)
{
if(intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
try {
BluetoothDevice bd = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
ILogPack logPack = new ILogPack(Phone.Keys.BLUETOOTH_DEVICE_ADDRESS, bd.getAddress());
logPack.put(Phone.Keys.BLUETOOTH_DEVICE_NAME, bd.getName());
onUpdate(logPack);
} catch(JSONException e) {}
} else if(intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
try {
ILogPack ILogPack = new ILogPack(Phone.Keys.VISIBLE_WIFI_NETWORKS, ((PhoneSucker) _phone).getWifiNetworks());
onUpdate(ILogPack);
} catch(NullPointerException e) {
Log.e(LOG, "CONSIDERED HANDLED:\n" + e.toString());
e.printStackTrace();
}
}
}
}
}
private void onCacheSaved(boolean restartCache) {
cacheTimer.cancel();
if(restartCache) {
initCache();
}
}
}