package eu.faircode.backpacktrack2; import android.content.ContentProviderClient; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.location.Location; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.preference.PreferenceManager; import android.util.Log; import com.google.android.gms.location.DetectedActivity; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.TimeZone; public class DatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "BPT2.Database"; private static final String DB_NAME = "BackPackTrackII"; private static final int DB_VERSION = 27; private static HandlerThread hthread = null; private static Handler handler = null; private final static int MSG_LOCATION_UPDATED = 1; private static List<LocationChangedListener> mLocationChangedListeners = new ArrayList<LocationChangedListener>(); private static List<ActivityTypeChangedListener> mActivityTypeChangedListeners = new ArrayList<ActivityTypeChangedListener>(); private static List<ActivityDurationChangedListener> mActivityDurationChangedListeners = new ArrayList<ActivityDurationChangedListener>(); private static List<ActivityLogChangedListener> mActivityLogChangedListeners = new ArrayList<ActivityLogChangedListener>(); private static List<StepCountChangedListener> mStepCountChangedListeners = new ArrayList<StepCountChangedListener>(); private static List<WeatherChangedListener> mWeatherChangedListeners = new ArrayList<WeatherChangedListener>(); private Context mContext; static { hthread = new HandlerThread("DatabaseHelper"); hthread.start(); handler = new Handler(hthread.getLooper()) { @Override public void handleMessage(Message msg) { handleChangedNotification(msg); } }; } public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); mContext = context; File oldName = context.getDatabasePath("BACKPACKTRACKII"); if (oldName.exists()) { File newName = new File(oldName.getParentFile(), DB_NAME); Log.i(TAG, "Renaming " + oldName.getAbsolutePath() + " to " + newName.getAbsolutePath()); oldName.renameTo(newName); } oldName = context.getDatabasePath("BACKPACKTRACKII-journal"); if (oldName.exists()) { File newName = new File(oldName.getParentFile(), DB_NAME + "-journal"); Log.i(TAG, "Renaming " + oldName.getAbsolutePath() + " to " + newName.getAbsolutePath()); oldName.renameTo(newName); } } @Override public void onCreate(SQLiteDatabase db) { Log.i(TAG, "Creating database " + DB_NAME + ":" + DB_VERSION); createTableLocation(db); createTableActivityType(db); createTableActivityDuration(db); createTableActivityLog(db); createTableStep(db); createTableWeather(db); } private void createTableLocation(SQLiteDatabase db) { db.execSQL("CREATE TABLE location (" + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + ", time INTEGER NOT NULL" + ", provider INTEGER NOT NULL" + ", latitude REAL NOT NULL" + ", longitude REAL NOT NULL" + ", altitude REAL NULL" + ", altitude_type INTEGER NULL" + ", speed REAL NULL" + ", bearing REAL NULL" + ", accuracy REAL NULL" + ", name TEXT" + ", proximity INTEGER NULL" + ", hidden INTEGER NULL" + ", deleted INTEGER NULL" + ", sent INTEGER NULL" + ");"); db.execSQL("CREATE INDEX idx_location_time ON location(time)"); db.execSQL("CREATE INDEX idx_location_name ON location(name)"); db.execSQL("CREATE INDEX idx_location_sent ON location(sent)"); } private void createTableActivityType(SQLiteDatabase db) { db.execSQL("CREATE TABLE activitytype (" + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + ", time INTEGER NOT NULL" + ", activity INTEGER NOT NULL" + ", confidence INTEGER NOT NULL" + ");"); db.execSQL("CREATE INDEX idx_activitytype_time ON activitytype(time)"); } private void createTableActivityDuration(SQLiteDatabase db) { Log.i(TAG, "Adding table activityduration"); db.execSQL("CREATE TABLE activityduration (" + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + ", time INTEGER NOT NULL" + ", still INTEGER NOT NULL" + ", walking INTEGER NOT NULL" + ", running INTEGER NOT NULL" + ", onbicycle INTEGER NOT NULL" + ", invehicle INTEGER NOT NULL" + ", unknown INTEGER NOT NULL" + ");"); db.execSQL("CREATE INDEX idx_activityduration_time ON activityduration(time)"); } private void createTableActivityLog(SQLiteDatabase db) { db.execSQL("CREATE TABLE activitylog (" + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + ", start INTEGER NOT NULL" + ", finish INTEGER NOT NULL" + ", activity INTEGER NOT NULL" + ");"); db.execSQL("CREATE INDEX idx_activitylog_start ON activitylog(start)"); db.execSQL("CREATE INDEX idx_activitylog_finish ON activitylog(finish)"); db.execSQL("CREATE INDEX idx_activitylog_activity ON activitylog(activity)"); } private void createTableStep(SQLiteDatabase db) { Log.i(TAG, "Adding table step"); db.execSQL("CREATE TABLE step (" + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + ", time INTEGER NOT NULL" + ", count INTEGER NOT NULL" + ");"); db.execSQL("CREATE INDEX idx_step_time ON step(time)"); } private void createTableWeather(SQLiteDatabase db) { Log.i(TAG, "Adding table weather"); db.execSQL("CREATE TABLE weather (" + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + ", time INTEGER NOT NULL" + ", provider TEXT NULL" + ", station_id INTEGER NOT NULL" + ", station_type INTEGER NOT NULL" + ", station_name TEXT NULL" + ", station_latitude REAL NULL" + ", station_longitude REAL NULL" + ", latitude REAL NULL" + ", longitude REAL NULL" + ", temperature REAL NULL" + ", humidity REAL NULL" + ", pressure REAL NULL" + ", wind_speed REAL NULL" + ", wind_gust REAL NULL" + ", wind_direction REAL NULL" + ", visibility REAL NULL" + ", rain_1h REAL NULL" + ", rain_today REAL NULL" + ", rain_probability REAL NULL" + ", clouds REAL NULL" + ", ozone REAL NULL" + ", icon TEXT NULL" + ", summary TEXT NULL" + ", created INTEGER NULL" + ");"); db.execSQL("CREATE INDEX idx_weather_time ON weather(time)"); db.execSQL("CREATE INDEX idx_weather_station_id ON weather(station_id)"); } private boolean columnExists(SQLiteDatabase db, String table, String column) { Cursor cursor = null; try { cursor = db.rawQuery("SELECT * FROM " + table + " LIMIT 0", null); return (cursor.getColumnIndex(column) >= 0); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); return false; } finally { if (cursor != null) cursor.close(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion); db.beginTransaction(); try { if (oldVersion < 2) { createTableActivityType(db); oldVersion = 2; } if (oldVersion < 3) { createTableStep(db); oldVersion = 3; } if (oldVersion < 4) { db.execSQL("ALTER TABLE location ADD COLUMN activity_type INTEGER NULL"); db.execSQL("ALTER TABLE location ADD COLUMN activity_confidence INTEGER NULL"); db.execSQL("ALTER TABLE location ADD COLUMN stepcount INTEGER NULL"); oldVersion = 4; } if (oldVersion < 5) { db.execSQL("UPDATE step SET time = time - " + TimeZone.getDefault().getOffset(new Date().getTime())); oldVersion = 5; } if (oldVersion < 7) { createTableActivityDuration(db); oldVersion = 7; } if (oldVersion < 8) { db.execSQL("ALTER TABLE activity RENAME TO activitytype"); // Index activity_time not renamed db.execSQL("ALTER TABLE activityduration RENAME TO activityduration_orig"); db.execSQL("DROP INDEX idx_activityduration_time"); createTableActivityDuration(db); db.execSQL( "INSERT INTO activityduration (time, still, walking, running, onbicycle, invehicle, unknown)" + " SELECT time, still, onfoot, running, onbicycle, invehicle, unknown FROM activityduration_orig"); db.execSQL("DROP TABLE activityduration_orig"); oldVersion = 8; } if (oldVersion < 9) { createTableActivityLog(db); oldVersion = 9; } if (oldVersion < 10) { createTableWeather(db); oldVersion = 10; } if (oldVersion < 11) { db.execSQL("ALTER TABLE weather ADD COLUMN station_latitude REAL NULL"); db.execSQL("ALTER TABLE weather ADD COLUMN station_longitude REAL NULL"); //db.execSQL("ALTER TABLE weather DROP COLUMN distance"); db.execSQL("UPDATE weather SET station_latitude = latitude"); db.execSQL("UPDATE weather SET station_longitude = longitude"); db.execSQL("UPDATE weather SET latitude = NULL"); db.execSQL("UPDATE weather SET longitude = NULL"); oldVersion = 11; } if (oldVersion < 12) { db.execSQL("ALTER TABLE weather RENAME TO weather_orig"); db.execSQL("DROP INDEX idx_weather_time"); db.execSQL("DROP INDEX idx_weather_station_id"); createTableWeather(db); db.execSQL( "INSERT INTO weather (time, station_id, station_type, station_name, station_latitude, station_longitude" + ", latitude, longitude, temperature, humidity, pressure, wind_speed, wind_direction, created)" + " SELECT time, station_id, station_type, station_name, station_latitude, station_longitude" + ", latitude, longitude, temperature, humidity, pressure, wind_speed, wind_direction, created FROM weather_orig"); db.execSQL("DROP TABLE weather_orig"); oldVersion = 12; } if (oldVersion < 13) { db.execSQL("ALTER TABLE weather ADD COLUMN rain_1h REAL NULL"); db.execSQL("ALTER TABLE weather ADD COLUMN rain_today REAL NULL"); oldVersion = 13; } if (oldVersion < 14) { db.execSQL("ALTER TABLE weather ADD COLUMN wind_gust REAL NULL"); oldVersion = 14; } if (oldVersion < 15) { db.execSQL("ALTER TABLE weather ADD COLUMN visibility REAL NULL"); oldVersion = 15; } if (oldVersion < 16) { db.execSQL("ALTER TABLE weather ADD COLUMN clouds REAL NULL"); oldVersion = 16; } if (oldVersion < 17) { db.execSQL("ALTER TABLE weather ADD COLUMN provider TEXT NULL"); db.execSQL("UPDATE weather SET provider = 'owm' WHERE station_id >= 0"); db.execSQL("UPDATE weather SET provider = 'fio' WHERE station_id < 0"); oldVersion = 17; } if (oldVersion < 18) { db.execSQL("ALTER TABLE weather ADD COLUMN icon TEXT NULL"); db.execSQL("ALTER TABLE weather ADD COLUMN summary TEXT NULL"); oldVersion = 18; } if (oldVersion < 19) { db.execSQL("ALTER TABLE weather ADD COLUMN rain_probability REAL NULL"); oldVersion = 19; } if (oldVersion < 20) { db.execSQL("ALTER TABLE location ADD COLUMN altitude_type INTEGER NULL"); oldVersion = 20; } if (oldVersion < 21) { db.execSQL("ALTER TABLE location ADD COLUMN hidden INTEGER NULL"); oldVersion = 21; } if (oldVersion < 22) { db.execSQL("ALTER TABLE weather ADD COLUMN ozone REAL NULL"); oldVersion = 22; } if (oldVersion < 23) { db.execSQL("ALTER TABLE location ADD COLUMN proximity INTEGER NULL"); oldVersion = 23; } if (oldVersion < 24) { db.execSQL("ALTER TABLE location RENAME TO location_orig"); db.execSQL("DROP INDEX idx_location_time"); db.execSQL("DROP INDEX idx_location_name"); createTableLocation(db); db.execSQL("INSERT INTO location (ID, time, provider, latitude, longitude, altitude, altitude_type, speed, bearing, accuracy, name, proximity, hidden)" + " SELECT ID, time, provider, latitude, longitude, altitude, altitude_type, speed, bearing, accuracy, name, proximity, hidden FROM location_orig"); db.execSQL("DROP TABLE location_orig"); oldVersion = 24; } if (oldVersion < 26) { if (!columnExists(db, "location", "sent")) { db.execSQL("ALTER TABLE location ADD COLUMN sent INTEGER NULL"); db.execSQL("CREATE INDEX idx_location_sent ON location(sent)"); } oldVersion = 26; } if (oldVersion < 27) { if (!columnExists(db, "location", "deleted")) db.execSQL("ALTER TABLE location ADD COLUMN deleted INTEGER NULL"); oldVersion = 27; } db.setVersion(DB_VERSION); db.setTransactionSuccessful(); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } finally { db.endTransaction(); } } // Location private ContentValues getLifelineLocation(long id, String name, Location location) throws JSONException { JSONObject jloc = new JSONObject(); if (name != null) jloc.put("name", name); jloc.put("lat", location.getLatitude()); jloc.put("lon", location.getLongitude()); if (location.hasAltitude()) jloc.put("alt", Math.round(location.getAltitude())); if (location.hasAccuracy()) jloc.put("acc", Math.round(location.getAccuracy())); ContentValues cv = new ContentValues(); cv.put("time", location.getTime()); cv.put("source", mContext.getPackageName()); cv.put("type", name == null ? "trackpoint" : "waypoint"); cv.put("data", jloc.toString()); cv.put("reference", Long.toString(id)); return cv; } public DatabaseHelper insertLocation(Location location, int altitude_type, String name) { long id; synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("time", location.getTime()); cv.put("provider", location.getProvider()); cv.put("latitude", location.getLatitude()); cv.put("longitude", location.getLongitude()); if (location.hasAltitude()) cv.put("altitude", location.getAltitude()); else cv.putNull("altitude"); cv.put("altitude_type", altitude_type); if (location.hasSpeed()) cv.put("speed", location.getSpeed()); else cv.putNull("speed"); if (location.hasBearing()) cv.put("bearing", location.getBearing()); else cv.putNull("bearing"); if (location.hasAccuracy()) cv.put("accuracy", location.getAccuracy()); else cv.putNull("accuracy"); if (name == null) cv.putNull("name"); else cv.put("name", name); id = db.insert("location", null, cv); if (id == -1) Log.e(TAG, "Insert location failed"); } if (id != -1) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); prefs.edit().putLong(SettingsFragment.PREF_LIFELINE_LAST, new Date().getTime()).apply(); Intent lifeline = new Intent(mContext, BackgroundService.class); lifeline.setAction(BackgroundService.ACTION_LIFELINE); lifeline.putExtra(BackgroundService.EXTRA_ID, id); mContext.startService(lifeline); try { Uri uri = Uri.parse("content://eu.faircode.lifeline/event"); ContentProviderClient cclient = mContext.getContentResolver().acquireContentProviderClient(uri); if (cclient != null) { Uri row = cclient.insert(uri, getLifelineLocation(id, name, location)); cclient.release(); Log.i(TAG, "Inserted uri=" + row); } } catch (Throwable ex) { Log.e(TAG, "Lifeline: " + ex.toString() + "\n" + Log.getStackTraceString(ex)); } for (LocationChangedListener listener : mLocationChangedListeners) try { listener.onLocationAdded(location); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } } return this; } public DatabaseHelper updateLocationName(long id, String name) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("name", name); cv.putNull("sent"); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location failed"); } notifyLocationUpdated(id); return this; } public DatabaseHelper updateLocationTime(long id, long time) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("time", time); cv.putNull("sent"); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location failed"); } notifyLocationUpdated(id); return this; } public DatabaseHelper updateLocationAltitude(long id, double altitude, int altitude_type) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("altitude", altitude); cv.put("altitude_type", altitude_type); cv.putNull("sent"); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location altitude failed"); } notifyLocationUpdated(id); return this; } public DatabaseHelper hideLocation(long id, boolean hidden) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("hidden", hidden ? 1 : 0); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location hidden failed"); } for (LocationChangedListener listener : mLocationChangedListeners) try { listener.onLocationUpdated(); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public DatabaseHelper sentLocation(long id, boolean sent) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); if (sent) cv.put("sent", 1); else cv.putNull("sent"); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location sent failed"); } for (LocationChangedListener listener : mLocationChangedListeners) try { listener.onLocationUpdated(); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public DatabaseHelper setProximity(long id, long radius) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); if (radius <= 0) cv.putNull("proximity"); else cv.put("proximity", radius); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location radius failed"); } notifyLocationUpdated(id); return this; } public DatabaseHelper deleteLocation(long id) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("deleted", 1); cv.putNull("sent"); if (db.update("location", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update location deleted failed"); } try { Uri uri = Uri.parse("content://eu.faircode.lifeline/event"); ContentProviderClient cclient = mContext.getContentResolver().acquireContentProviderClient(uri); if (cclient != null) { int rows = cclient.delete(uri, "reference = ?", new String[]{Long.toString(id)}); cclient.release(); Log.i(TAG, "Delete uri=" + uri + " rows=" + rows); } } catch (Throwable ex) { Log.e(TAG, "Lifeline: " + ex.toString() + "\n" + Log.getStackTraceString(ex)); } for (LocationChangedListener listener : mLocationChangedListeners) try { listener.onLocationDeleted(id); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); prefs.edit().putLong(SettingsFragment.PREF_LIFELINE_LAST, new Date().getTime()).apply(); Intent lifeline = new Intent(mContext, BackgroundService.class); lifeline.setAction(BackgroundService.ACTION_LIFELINE); lifeline.putExtra(BackgroundService.EXTRA_ID, id); mContext.startService(lifeline); return this; } public DatabaseHelper deleteTrackpoints(long from, long to) { synchronized (mContext.getApplicationContext()) { Log.i(TAG, "Delete from=" + from + " to=" + to); SQLiteDatabase db = this.getWritableDatabase(); int rows = db.delete("location", "time >= ? AND time <= ? AND name IS NULL", new String[]{Long.toString(from), Long.toString(to)}); Log.i(TAG, rows + " trackpoints deleted"); } for (LocationChangedListener listener : mLocationChangedListeners) try { listener.onLocationDeleted(-1); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public Cursor getLocations(long from, long to, boolean trackpoints, boolean waypoints, boolean asc, int limit) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT *, ID AS _id FROM location"; query += " WHERE time >= ? AND time <= ? AND deleted IS NULL"; if (trackpoints && !waypoints) query += " AND name IS NULL"; if (!trackpoints && waypoints) query += " AND NOT name IS NULL"; query += " ORDER BY time"; if (!asc) query += " DESC"; if (limit > 0) query += " LIMIT " + limit; return db.rawQuery(query, new String[]{Long.toString(from), Long.toString(to)}); } public Cursor getWaypoints() { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT ID AS _id, latitude, longitude, name"; query += " FROM location"; query += " WHERE NOT name IS NULL AND (hidden IS NULL OR hidden = 0) AND deleted IS NULL"; query += " ORDER BY name"; return db.rawQuery(query, new String[0]); } public Cursor getUnsentLocations() { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT * FROM location"; query += " WHERE sent IS NULL"; query += " ORDER BY time DESC"; return db.rawQuery(query, new String[]{}); } public int getUnsentLocationCount() { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT COUNT(*) FROM location"; query += " WHERE sent IS NULL"; Cursor cursor = null; try { cursor = db.rawQuery(query, new String[]{}); if (cursor.moveToNext()) return cursor.getInt(0); else return 0; } finally { if (cursor != null) cursor.close(); } } public Location getLocation(long id) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT * FROM location"; query += " WHERE ID = " + id; Cursor cursor = null; try { cursor = db.rawQuery(query, new String[0]); if (cursor.moveToNext()) { int colTime = cursor.getColumnIndex("time"); int colLatitude = cursor.getColumnIndex("latitude"); int colLongitude = cursor.getColumnIndex("longitude"); int colAltitude = cursor.getColumnIndex("altitude"); int colAccuracy = cursor.getColumnIndex("accuracy"); int colName = cursor.getColumnIndex("name"); int colDeleted = cursor.getColumnIndex("deleted"); Location location = new Location(cursor.getString(colName)); // hack location.setTime(cursor.isNull(colDeleted) ? cursor.getLong(colTime) : Long.MAX_VALUE); location.setLatitude(cursor.getDouble(colLatitude)); location.setLongitude(cursor.getDouble(colLongitude)); if (!cursor.isNull(colAltitude)) location.setAltitude(cursor.getDouble(colAltitude)); if (!cursor.isNull(colAccuracy)) location.setAccuracy(cursor.getFloat(colAccuracy)); return location; } else return null; } finally { if (cursor != null) cursor.close(); } } // Activity public DatabaseHelper insertActivityType(long time, int activity, int confidence) { long id = -1; synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("time", time); cv.put("activity", activity); cv.put("confidence", confidence); id = db.insert("activitytype", null, cv); if (id == -1) Log.e(TAG, "Insert activity type failed"); } if (id != -1) for (ActivityTypeChangedListener listener : mActivityTypeChangedListeners) try { listener.onActivityAdded(time, activity, confidence); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public DatabaseHelper deleteActivityTypes() { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); db.delete("activitytype", null, new String[]{}); } for (ActivityTypeChangedListener listener : mActivityTypeChangedListeners) try { listener.onActivityDeleted(-1); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public Cursor getActivityTypes(long from, long to) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT *, ID AS _id FROM activitytype"; query += " WHERE time >= ? AND time <= ?"; query += " ORDER BY time DESC"; return db.rawQuery(query, new String[]{Long.toString(from), Long.toString(to)}); } // Activity duration public DatabaseHelper updateActivity(long time, int activity, long duration) { // Activity duration int prev = -1; long day = getDay(time); String column; synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); switch (activity) { case DetectedActivity.STILL: column = "still"; break; case DetectedActivity.ON_FOOT: case DetectedActivity.WALKING: column = "walking"; break; case DetectedActivity.RUNNING: column = "running"; break; case DetectedActivity.ON_BICYCLE: column = "onbicycle"; break; case DetectedActivity.IN_VEHICLE: column = "invehicle"; break; default: column = "unknown"; break; } Cursor c = null; try { c = db.query("activityduration", new String[]{column}, "time = ?", new String[]{Long.toString(day)}, null, null, null, null); if (c.moveToFirst()) prev = c.getInt(c.getColumnIndex(column)); } finally { if (c != null) c.close(); } if (prev < 0) { Log.i(TAG, "Creating new day time=" + day); ContentValues cv = new ContentValues(); cv.put("time", day); cv.put("still", 0); cv.put("walking", 0); cv.put("running", 0); cv.put("onbicycle", 0); cv.put("invehicle", 0); cv.put("unknown", 0); if (db.insert("activityduration", null, cv) == -1) Log.e(TAG, "Insert activity duration failed"); } if (duration > 0) { ContentValues cv = new ContentValues(); cv.put(column, prev + duration); if (db.update("activityduration", cv, "time = ?", new String[]{Long.toString(day)}) != 1) Log.e(TAG, "Update activity duration failed"); } } if (prev < 0) for (ActivityDurationChangedListener listener : mActivityDurationChangedListeners) try { listener.onActivityAdded(day); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } else if (duration > 0) for (ActivityDurationChangedListener listener : mActivityDurationChangedListeners) try { listener.onActivityUpdated(day, activity, prev + duration); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } // Activity log long start = -1; synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); Cursor c = null; try { c = db.query("activitylog", new String[]{"start"}, "finish = ? AND activity = ?", new String[]{Long.toString(time), Integer.toString(activity)}, null, null, null, null); if (c.moveToFirst()) start = c.getLong(c.getColumnIndex("start")); } finally { if (c != null) c.close(); } if (start < 0) { ContentValues cv = new ContentValues(); cv.put("start", time); cv.put("finish", time + duration); cv.put("activity", activity); if (db.insert("activitylog", null, cv) == -1) Log.e(TAG, "Insert activity log failed"); } else { ContentValues cv = new ContentValues(); cv.put("finish", time + duration); if (db.update("activitylog", cv, "start = ?", new String[]{Long.toString(start)}) != 1) Log.e(TAG, "Update activity log failed"); } } if (start < 0) for (ActivityLogChangedListener listener : mActivityLogChangedListeners) try { listener.onActivityAdded(time, time + duration, activity); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } else for (ActivityLogChangedListener listener : mActivityLogChangedListeners) try { listener.onActivityUpdated(start, time + duration, activity); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public DatabaseHelper deleteActivity(long id) { // This will not delete the activity log synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); if (db.delete("activityduration", "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Delete activity duration failed"); } for (ActivityDurationChangedListener listener : mActivityDurationChangedListeners) try { listener.onActivityDeleted(id); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public Cursor getActivityDurations(long from, long to, boolean asc) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT *, ID AS _id FROM activityduration"; query += " WHERE time >= ? AND time <= ?"; query += " ORDER BY time"; if (!asc) query += " DESC"; return db.rawQuery(query, new String[]{Long.toString(from), Long.toString(to)}); } public Cursor getActivityLog(long from, long to, boolean asc) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT *, ID AS _id FROM activitylog"; query += " WHERE start <= ? AND finish >= ?"; query += " ORDER BY start"; if (!asc) query += " DESC"; return db.rawQuery(query, new String[]{Long.toString(to), Long.toString(from)}); } // Steps public DatabaseHelper updateSteps(long time, int delta) { int count = -1; long day = getDay(time); synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); Cursor c = null; try { c = db.query("step", new String[]{"count"}, "time = ?", new String[]{Long.toString(day)}, null, null, null, null); if (c.moveToFirst()) count = c.getInt(c.getColumnIndex("count")); } finally { if (c != null) c.close(); } if (count < 0) { Log.i(TAG, "Creating new day time=" + day); ContentValues cv = new ContentValues(); cv.put("time", day); cv.put("count", delta); if (db.insert("step", null, cv) == -1) Log.e(TAG, "Insert step failed"); } else { ContentValues cv = new ContentValues(); cv.put("count", count + delta); if (db.update("step", cv, "time = ?", new String[]{Long.toString(day)}) != 1) Log.e(TAG, "Update step failed"); } } for (StepCountChangedListener listener : mStepCountChangedListeners) try { if (count < 0) listener.onStepCountAdded(day, delta); else listener.onStepCountUpdated(day, count + delta); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public DatabaseHelper updateSteps(long id, long time, int value) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("count", value); if (db.update("step", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Update step failed"); } for (StepCountChangedListener listener : mStepCountChangedListeners) try { listener.onStepCountUpdated(time, value); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public DatabaseHelper deleteStep(long id) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); if (db.delete("step", "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Delete step failed"); } for (StepCountChangedListener listener : mStepCountChangedListeners) try { listener.onStepDeleted(id); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public Cursor getSteps(long from, long to, boolean asc) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT *, ID AS _id FROM step"; query += " WHERE time >= ? AND time <= ?"; query += " ORDER BY time"; if (!asc) query += " DESC"; return db.rawQuery(query, new String[]{Long.toString(from), Long.toString(to)}); } public int getSteps(long time) { long day = getDay(time); SQLiteDatabase db = this.getReadableDatabase(); Cursor c = null; try { c = db.query("step", new String[]{"count"}, "time = ?", new String[]{Long.toString(day)}, null, null, "time DESC", null); if (c.moveToFirst()) return c.getInt(c.getColumnIndex("count")); else return 0; } finally { if (c != null) c.close(); } } // Weather public boolean insertWeather(Weather weather, Location location) { long id = -1; synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); Cursor c = null; try { c = db.query("weather", new String[]{"ID"}, "time = ? AND station_id = ?", new String[]{Long.toString(weather.time), Long.toString(weather.station_id)}, null, null, null, null); if (c.getCount() != 0) return false; } finally { if (c != null) c.close(); } ContentValues cv = new ContentValues(); cv.put("time", weather.time); cv.put("provider", weather.provider); cv.put("station_id", weather.station_id); cv.put("station_type", weather.station_type); cv.put("station_name", weather.station_name); if (weather.station_location == null) { cv.putNull("station_latitude"); cv.putNull("station_longitude"); } else { cv.put("station_latitude", weather.station_location.getLatitude()); cv.put("station_longitude", weather.station_location.getLongitude()); } if (location == null) { cv.putNull("latitude"); cv.putNull("longitude"); } else { cv.put("latitude", location.getLatitude()); cv.put("longitude", location.getLongitude()); } if (Double.isNaN(weather.temperature)) cv.putNull("temperature"); else cv.put("temperature", weather.temperature); if (Double.isNaN(weather.humidity)) cv.putNull("humidity"); else cv.put("humidity", weather.humidity); if (Double.isNaN(weather.pressure)) cv.putNull("pressure"); else cv.put("pressure", weather.pressure); if (Double.isNaN(weather.wind_speed)) cv.putNull("wind_speed"); else cv.put("wind_speed", weather.wind_speed); if (Double.isNaN(weather.wind_gust)) cv.putNull("wind_gust"); else cv.put("wind_gust", weather.wind_gust); if (Double.isNaN(weather.wind_direction)) cv.putNull("wind_direction"); else cv.put("wind_direction", weather.wind_direction); if (Double.isNaN(weather.visibility)) cv.putNull("visibility"); else cv.put("visibility", weather.visibility); if (Double.isNaN(weather.rain_1h)) cv.putNull("rain_1h"); else cv.put("rain_1h", weather.rain_1h); if (Double.isNaN(weather.rain_today)) cv.putNull("rain_today"); else cv.put("rain_today", weather.rain_today); if (Double.isNaN(weather.rain_probability)) cv.putNull("rain_probability"); else cv.put("rain_probability", weather.rain_probability); if (Double.isNaN(weather.clouds)) cv.putNull("clouds"); else cv.put("clouds", weather.clouds); if (Double.isNaN(weather.ozone)) cv.putNull("ozone"); else cv.put("ozone", weather.ozone); if (weather.icon == null) cv.putNull("icon"); else cv.put("icon", weather.icon); if (weather.summary == null) cv.putNull("summary"); else cv.put("summary", weather.summary); cv.put("created", new Date().getTime()); id = db.insert("weather", null, cv); if (id == -1) Log.e(TAG, "Insert weather failed"); else Log.i(TAG, "Stored " + weather); } if (id != -1) { if (location != null && "fio".equals(weather.provider)) try { ContentValues cv = new ContentValues(); cv.put("time", weather.time); cv.put("source", mContext.getPackageName()); cv.put("type", "darksky"); cv.put("data", weather.rawData); cv.put("reference", Long.toString(id)); Uri uri = Uri.parse("content://eu.faircode.lifeline/event"); ContentProviderClient cclient = mContext.getContentResolver().acquireContentProviderClient(uri); if (cclient != null) { Uri row = cclient.insert(uri, cv); cclient.release(); Log.i(TAG, "Inserted uri=" + row); } } catch (Throwable ex) { Log.e(TAG, "Lifeline: " + ex.toString() + "\n" + Log.getStackTraceString(ex)); } for (WeatherChangedListener listener : mWeatherChangedListeners) try { listener.onWeatherAdded(weather.time, weather.station_id); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } } return true; } public DatabaseHelper deleteWeather(long id) { synchronized (mContext.getApplicationContext()) { SQLiteDatabase db = this.getWritableDatabase(); if (db.delete("weather", "ID = ?", new String[]{Long.toString(id)}) != 1) Log.e(TAG, "Delete weather failed"); } for (WeatherChangedListener listener : mWeatherChangedListeners) try { listener.onWeatherDeleted(id); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } return this; } public Cursor getWeather(long from, long to, boolean asc) { SQLiteDatabase db = this.getReadableDatabase(); String query = "SELECT *, ID AS _id FROM weather"; query += " WHERE time >= ? AND time <= ?"; query += " ORDER BY time"; if (!asc) query += " DESC"; return db.rawQuery(query, new String[]{Long.toString(from), Long.toString(to)}); } // Utility public DatabaseHelper vacuum() { SQLiteDatabase db = this.getWritableDatabase(); Log.i(TAG, "Running vacuum"); db.execSQL("VACUUM"); return this; } // Helper methods private long getDay(long ms) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(ms); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTimeInMillis(); } // Changes public static void addLocationChangedListener(LocationChangedListener listener) { mLocationChangedListeners.add(listener); } public static void removeLocationChangedListener(LocationChangedListener listener) { mLocationChangedListeners.remove(listener); } public static void addActivityTypeChangedListener(ActivityTypeChangedListener listener) { mActivityTypeChangedListeners.add(listener); } public static void removeActivityTypeChangedListener(ActivityTypeChangedListener listener) { mActivityTypeChangedListeners.remove(listener); } public static void addActivityDurationChangedListener(ActivityDurationChangedListener listener) { mActivityDurationChangedListeners.add(listener); } public static void removeActivityDurationChangedListener(ActivityDurationChangedListener listener) { mActivityDurationChangedListeners.remove(listener); } public static void addActivityLogChangedListener(ActivityLogChangedListener listener) { mActivityLogChangedListeners.add(listener); } public static void removeActivityLogChangedListener(ActivityLogChangedListener listener) { mActivityLogChangedListeners.remove(listener); } public static void addStepCountChangedListener(StepCountChangedListener listener) { mStepCountChangedListeners.add(listener); } public static void removeStepCountChangedListener(StepCountChangedListener listener) { mStepCountChangedListeners.remove(listener); } public static void addWeatherChangedListener(WeatherChangedListener listener) { mWeatherChangedListeners.add(listener); } public static void removeWeatherChangedListener(WeatherChangedListener listener) { mWeatherChangedListeners.remove(listener); } private void notifyLocationUpdated(long id) { Message msg = handler.obtainMessage(); msg.what = MSG_LOCATION_UPDATED; msg.obj = (Long) id; handler.sendMessage(msg); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); prefs.edit().putLong(SettingsFragment.PREF_LIFELINE_LAST, new Date().getTime()).apply(); Intent lifeline = new Intent(mContext, BackgroundService.class); lifeline.setAction(BackgroundService.ACTION_LIFELINE); lifeline.putExtra(BackgroundService.EXTRA_ID, id); mContext.startService(lifeline); try { Uri uri = Uri.parse("content://eu.faircode.lifeline/event"); ContentProviderClient cclient = mContext.getContentResolver().acquireContentProviderClient(uri); if (cclient != null) { Location location = getLocation(id); Uri row = cclient.insert(uri, getLifelineLocation(id, location.getProvider(), location)); cclient.release(); Log.i(TAG, "Updated uri=" + row); } } catch (Throwable ex) { Log.e(TAG, "Lifeline: " + ex.toString() + "\n" + Log.getStackTraceString(ex)); } } private static void handleChangedNotification(Message msg) { // Batch notifications try { Thread.sleep(2500); if (handler.hasMessages(msg.what)) handler.removeMessages(msg.what); } catch (InterruptedException ignored) { } // Notify listeners if (msg.what == MSG_LOCATION_UPDATED) { for (LocationChangedListener listener : mLocationChangedListeners) try { listener.onLocationUpdated(); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } } } public interface LocationChangedListener { void onLocationAdded(Location location); void onLocationUpdated(); void onLocationDeleted(long id); } public interface ActivityTypeChangedListener { void onActivityAdded(long time, int activity, int confidence); void onActivityDeleted(long id); } public interface ActivityDurationChangedListener { void onActivityAdded(long day); void onActivityUpdated(long day, int activity, long duration); void onActivityDeleted(long id); } public interface ActivityLogChangedListener { void onActivityAdded(long start, long finish, int activity); void onActivityUpdated(long start, long finish, int activity); } public interface StepCountChangedListener { void onStepCountAdded(long time, int count); void onStepCountUpdated(long time, int count); void onStepDeleted(long id); } public interface WeatherChangedListener { void onWeatherAdded(long time, long station_id); void onWeatherDeleted(long id); } }