diff --git a/build.gradle b/build.gradle index db0b2c0..369f7f5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 24 - buildToolsVersion "25.0.0" + compileSdkVersion project.appCompileSdkVersion + buildToolsVersion project.appBuildToolVersion publishNonDefault true defaultConfig { minSdkVersion 15 - targetSdkVersion project.appTargetSdkVersion + targetSdkVersion 23 versionCode 1 versionName "1.0" @@ -22,11 +22,18 @@ android { } } +repositories { + maven { url 'https://maven.fabric.io/public' } +} + dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile 'com.android.support:appcompat-v7:23.1.1' + compile "com.android.support:appcompat-v7:${project.supportLibVersion}" testCompile 'junit:junit:4.12' + compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar') { + transitive = true; + } } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index c90e544..5fd4509 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -6,8 +6,8 @@ android:label="@string/app_name" android:supportsRtl="true"> + android:name=".SensorListener" + android:exported="false" /> diff --git a/src/main/java/com/sukesan1984/stepsensorlib/BootReceiver.java b/src/main/java/com/sukesan1984/stepsensorlib/BootReceiver.java index 162d9fd..6669256 100644 --- a/src/main/java/com/sukesan1984/stepsensorlib/BootReceiver.java +++ b/src/main/java/com/sukesan1984/stepsensorlib/BootReceiver.java @@ -4,36 +4,9 @@ import android.content.Context; import android.content.Intent; -import com.sukesan1984.stepsensorlib.util.DateUtils; -import com.sukesan1984.stepsensorlib.util.Logger; -import com.sukesan1984.stepsensorlib.util.SensorListener; - -/** - * Created by kosuketakami on 2016/11/05. - */ - public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - Database db = Database.getInstance(context); - - if (!PreferenceManager.readCorrectShutDown(context, false)) { - if (BuildConfig.DEBUG) { - Logger.log("Incorrect shutdown"); - } - int steps = db.getLastUpdatedSteps(); - if (BuildConfig.DEBUG) { - Logger.log("Trying to recover " + steps + " steps"); - } - db.updateOrInsert(DateUtils.getCurrentDateAndHour(), steps); - } - // last entry might still have a negative step value, so remove that - // row if that's the case - db.removeNegativeEntries(); - db.close(); - - PreferenceManager.deleteCorrectShutDown(context); - PreferenceManager.deleteStepsSinceBoot(context); context.startService(new Intent(context, SensorListener.class)); } } diff --git a/src/main/java/com/sukesan1984/stepsensorlib/Database.java b/src/main/java/com/sukesan1984/stepsensorlib/Database.java index f328a93..fb2ef5f 100644 --- a/src/main/java/com/sukesan1984/stepsensorlib/Database.java +++ b/src/main/java/com/sukesan1984/stepsensorlib/Database.java @@ -6,29 +6,22 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.support.annotation.NonNull; - +import android.support.annotation.Nullable; import com.sukesan1984.stepsensorlib.model.ChunkStepCount; import com.sukesan1984.stepsensorlib.util.DateUtils; import com.sukesan1984.stepsensorlib.util.Logger; - import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -/** - * - */ - -public class Database extends SQLiteOpenHelper { +class Database extends SQLiteOpenHelper { private final static String TABLE_NAME = "steps"; private final static String COLUMN_DATE_AND_HOUR = "date_and_hour"; private final static String COLUMN_STEPS = "steps"; private final static String COLUMN_IS_RECORDED_ON_SERVER = "is_recorded_on_server"; private final static String COLUMN_LAST_UPDATED = "last_updated"; - private final static int DB_VERSION = 1; + private final static int DB_VERSION = 2; private static Database instance; - private static final AtomicInteger openCounter = new AtomicInteger(); private Database(final Context context) { super(context, TABLE_NAME, null, DB_VERSION); @@ -38,21 +31,14 @@ public static synchronized Database getInstance(final Context c) { if (instance == null) { instance = new Database(c.getApplicationContext()); } - - openCounter.incrementAndGet(); - return instance; } - @Override - public void close() { - if (openCounter.decrementAndGet() == 0) { - super.close(); - } - } - @Override public void onCreate(SQLiteDatabase db) { + if (db == null) { + return; + } db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + COLUMN_DATE_AND_HOUR + " INTEGER UNIQUE, " + COLUMN_STEPS + " INTEGER, " + @@ -64,254 +50,132 @@ public void onCreate(SQLiteDatabase db) { * delete all */ public void deleteAll() { - SQLiteDatabase db = getWritableDatabase(); - db.delete(TABLE_NAME, "", new String[]{}); - db.close(); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // on upgrade - } - - /** - * Query the 'steps' table. Remember to close the cursor! - * - * @param columns the colums - * @param selection the selection - * @param selectionArgs the selction arguments - * @param groupBy the group by statement - * @param having the having statement - * @param orderBy the order by statement - * @return the cursor - */ - public Cursor query(final String[] columns, final String selection, - final String[] selectionArgs, final String groupBy, final String having, - final String orderBy, final String limit) { - return getReadableDatabase() - .query(TABLE_NAME, columns, selection, selectionArgs, groupBy, having, orderBy, limit); - } - - /** - * @param dateAndHour the dateAndHour in ms since 1970 - * @param stepsSinceBoot the steps since boot - */ - public void updateOrInsert(long dateAndHour, int stepsSinceBoot) { - SQLiteDatabase db = getWritableDatabase(); - db.beginTransaction(); + SQLiteDatabase db = null; try { - updateOrInsertWithoutTransaction(db, dateAndHour, stepsSinceBoot); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); + db = getWritableDatabase(); + db.delete(TABLE_NAME, "", new String[]{}); + } catch (Exception e) { + e.printStackTrace(); } } - private void updateOrInsertWithoutTransaction(SQLiteDatabase db, long dateAndHour, int stepsSinceBoot) { - Cursor c = getByDateAndHour(dateAndHour); - initializeLastUpdatedSteps(db, stepsSinceBoot); - int lastUpdatedSteps = getLastUpdatedSteps(); - if (lastUpdatedSteps > stepsSinceBoot) { - saveLastUpdatedSteps(db, stepsSinceBoot); - return; - } - - - int addedSteps = stepsSinceBoot - lastUpdatedSteps; - if (c.getCount() == 0 && stepsSinceBoot >= 0) { - insertNewDateAndHour(db, dateAndHour, addedSteps); - } else { - addToLastEntry(db, addedSteps); + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2) { + db.delete("steps", "date_and_hour = ?", new String[]{"-1"}); } - saveLastUpdatedSteps(db, stepsSinceBoot); } /** - * replace steps count if steps is larger than current databace value. + * Add step count in table. If no row matching to targetDateAndHour is exist, it will be created. * - * @param dateAndHour - * @param steps + * @param targetDateAndHour Key for table. + * @param stepsToAdd Count to be added. + * @return Total count after add, or negative value when failed. + * @throws IllegalArgumentException if stepsToAdd is negative value. */ - public void insertOrReplaceSteps(final long dateAndHour, final int steps) { - SQLiteDatabase db = getWritableDatabase(); - Cursor c = getByDateAndHour(dateAndHour); - db.beginTransaction(); + public int addSteps(long targetDateAndHour, int stepsToAdd) { + if (stepsToAdd < 0) throw new IllegalArgumentException("stepsToAdd should not be negative value."); + SQLiteDatabase db = null; try { - if (c.getCount() == 0) { - if (steps > 0) { - insertNewDateAndHour(db, dateAndHour, steps); - } else { - return; - } - } else if (c.moveToFirst() && c.getInt(0) < steps) { - Logger.log("#### update"); - Logger.log("#### data_and_hour: " + dateAndHour); - Logger.log("#### steps: " + steps); - ContentValues values = new ContentValues(); - values.put(COLUMN_DATE_AND_HOUR, dateAndHour); - values.put(COLUMN_STEPS, steps); - values.put(COLUMN_LAST_UPDATED, DateUtils.getCurrentTimeMllis()); - db.update(TABLE_NAME, values, - COLUMN_DATE_AND_HOUR + " = ?", - new String[]{String.valueOf(dateAndHour)}); - } + db = getWritableDatabase(); + db.beginTransaction(); + int currentSteps = getStepsImpl(db, targetDateAndHour); + int newSteps = currentSteps + stepsToAdd; + insertOrReplaceStepRow(db, targetDateAndHour, newSteps, false); db.setTransactionSuccessful(); + return newSteps; + } catch (Exception e) { + e.printStackTrace(); + return -1; } finally { - db.endTransaction(); + if (db != null) { + db.endTransaction(); + } } - } - /** - * Inserts a new entry in the database, if there is no entry for the given - * dateAndHour yet. - *

- * - * @param dateAndHour the dateAndHour in ms since 1970 - * @param steps the current step value - */ - - private void insertNewDateAndHour(SQLiteDatabase db, final long dateAndHour, final int steps) { - Logger.log("insert new date and hour"); - Logger.log("date_and_hour: " + dateAndHour); - Logger.log("steps: " + steps); + private void insertOrReplaceStepRow(SQLiteDatabase db, long dateAndHour, int steps, @Nullable Boolean markAsRecorded) { ContentValues values = new ContentValues(); values.put(COLUMN_DATE_AND_HOUR, dateAndHour); - // use the negative steps as offset values.put(COLUMN_STEPS, steps); values.put(COLUMN_LAST_UPDATED, DateUtils.getCurrentTimeMllis()); - db.insert(TABLE_NAME, null, values); - if (BuildConfig.DEBUG) { - Logger.log("insertDayAndHour" + dateAndHour + " / " + steps); - logState(); + if (markAsRecorded != null) { + values.put(COLUMN_IS_RECORDED_ON_SERVER, markAsRecorded ? 1 : 0); } + db.replaceOrThrow(TABLE_NAME, null, values); } - private Cursor getByDateAndHour(long dateAndHour) { - return getReadableDatabase().query(TABLE_NAME, new String[]{COLUMN_STEPS}, - COLUMN_DATE_AND_HOUR + " = ?", - new String[]{String.valueOf(dateAndHour)}, null, null, null); - } - - private void addToLastEntry(SQLiteDatabase db, int steps) { - if (steps > 0) { - db.execSQL("UPDATE " + TABLE_NAME + " SET " + COLUMN_STEPS + " = " + COLUMN_STEPS + " + " + steps - + ", " + COLUMN_IS_RECORDED_ON_SERVER + " = 0, " + COLUMN_LAST_UPDATED + " =" + DateUtils.getCurrentTimeMllis() + - " WHERE " + COLUMN_DATE_AND_HOUR + "= (SELECT MAX(" + COLUMN_DATE_AND_HOUR + ") FROM " + TABLE_NAME + ");"); - } - } - - /** - * Writes the current steps database to the log. - */ - public void logState() { - if (BuildConfig.DEBUG) { - Cursor c = getReadableDatabase() - .query(TABLE_NAME, null, null, null, null, null, COLUMN_DATE_AND_HOUR + " DESC", null); - Logger.log(c); - c.close(); - } - } - - /** - * @param dateAndHour - * @return - */ public int getSteps(final long dateAndHour) { Logger.log("getStep dateAndHour" + dateAndHour); - Cursor c = getByDateAndHour(dateAndHour); - - c.moveToFirst(); - int steps; - if (c.getCount() == 0) { - steps = Integer.MIN_VALUE; - } else { - steps = c.getInt(0); - } - c.close(); - return steps; + SQLiteDatabase db = getReadableDatabase(); + return getStepsImpl(db, dateAndHour); } - public int getSteps(final long start, final long end) { - Cursor c = getReadableDatabase() - .query(TABLE_NAME, new String[]{"SUM(" + COLUMN_STEPS + ")"}, - COLUMN_DATE_AND_HOUR + " >= ? AND " + - COLUMN_DATE_AND_HOUR + " <= ?", - new String[]{String.valueOf(start), String.valueOf(end)}, null, null, null); - int sumSteps; - if (c.getCount() == 0) { - sumSteps = 0; - } else { + private int getStepsImpl(SQLiteDatabase db, long dateAndHour) { + Cursor c = db.query(TABLE_NAME, new String[]{COLUMN_STEPS}, + COLUMN_DATE_AND_HOUR + " = ?", + new String[]{String.valueOf(dateAndHour)}, null, null, null); + try { + if (c == null) { + return 0; + } c.moveToFirst(); - sumSteps = c.getInt(0); + if (c.getCount() == 0) { + return 0; + } + return c.getInt(0); + } finally { + closeCursor(c); } - c.close(); - return sumSteps; - } - - public List getChunkStepsFrom(final long start) { - Cursor c = getReadableDatabase() - .query(TABLE_NAME, new String[]{COLUMN_DATE_AND_HOUR, COLUMN_STEPS}, - COLUMN_DATE_AND_HOUR + " >= ?", new String[]{String.valueOf(start)}, null, null, null); - return createChunkedStepCounts(c); - } - - /** - * Removes all entries with negative values. - *

- * Only call this directly after boot, otherwise it might remove the current - * day as the current offset is likely to be negative. - */ - void removeNegativeEntries() { - getWritableDatabase().delete(TABLE_NAME, COLUMN_STEPS + " < ?", new String[]{"0"}); - } - - /** - * Remove invalid entries from the database. - * Currently, an invalid input is such with steps >= 200,000 - */ - public void removeInvalidEntries() { - getWritableDatabase().delete(TABLE_NAME, COLUMN_STEPS + " >= ?", new String[]{"200000"}); } - /** - * @param stepsSinceBoot steps after boot. - */ - public void resetLastUpdatedSteps(final int stepsSinceBoot) { - SQLiteDatabase db = getWritableDatabase(); - db.beginTransaction(); + public int getSteps(final long start, final long end) { + Cursor c = null; try { - updateOrInsertWithoutTransaction(db, DateUtils.getCurrentDateAndHour(), stepsSinceBoot); - saveLastUpdatedSteps(db, 0); - db.setTransactionSuccessful(); + c = getReadableDatabase() + .query(TABLE_NAME, new String[]{"SUM(" + COLUMN_STEPS + ")"}, + COLUMN_DATE_AND_HOUR + " >= ? AND " + + COLUMN_DATE_AND_HOUR + " <= ?", + new String[]{String.valueOf(start), String.valueOf(end)}, null, null, null); + if (c == null) { + return 0; + } + int sumSteps; + if (c.getCount() == 0) { + sumSteps = 0; + } else { + c.moveToFirst(); + sumSteps = c.getInt(0); + } + return sumSteps; + } catch (Exception e) { + e.printStackTrace(); + return 0; } finally { - db.endTransaction(); + closeCursor(c); } } - private void initializeLastUpdatedSteps(SQLiteDatabase db, final int stepsSinceBoot) { - // initialize if there is no date - if (getSteps(-1) == Integer.MIN_VALUE) { - saveLastUpdatedSteps(db, stepsSinceBoot); - } - } + @NonNull + public List getChunkStepsSince(final long start) { + Cursor c = null; + try { + c = getReadableDatabase() + .query(TABLE_NAME, new String[]{COLUMN_DATE_AND_HOUR, COLUMN_STEPS}, + COLUMN_DATE_AND_HOUR + " >= ?", new String[]{String.valueOf(start)}, null, null, null); - private void saveLastUpdatedSteps(SQLiteDatabase db, final int steps) { - ContentValues values = new ContentValues(); - values.put(COLUMN_STEPS, steps); - if (db.update(TABLE_NAME, values, COLUMN_DATE_AND_HOUR + " = -1", null) == 0) { - values.put(COLUMN_DATE_AND_HOUR, -1); - values.put(COLUMN_LAST_UPDATED, DateUtils.getCurrentTimeMllis()); - db.insert(TABLE_NAME, null, values); - } - if (BuildConfig.DEBUG) { - Logger.log("saving steps in db: " + steps); - } - } + if (c == null) { + return new ArrayList<>(); + } - public int getLastUpdatedSteps() { - int currentSteps = getSteps(-1); - return currentSteps == Integer.MIN_VALUE ? 0 : currentSteps; + return createChunkedStepCounts(c); + } catch (Exception e) { + e.printStackTrace(); + return new ArrayList<>(); + } finally { + closeCursor(c); + } } public int getTodayStep() { @@ -320,16 +184,29 @@ public int getTodayStep() { } @NonNull - public List getNotRecordedChunkedStepCounts() { - Cursor c = getReadableDatabase() - .query(TABLE_NAME, new String[]{COLUMN_DATE_AND_HOUR, COLUMN_STEPS}, - COLUMN_DATE_AND_HOUR + " != ? and " + - COLUMN_IS_RECORDED_ON_SERVER + " = ?", new String[]{"-1", "0"}, null, null, null); - Logger.log("Not recoreded Chunk Size: " + c.getCount()); - return createChunkedStepCounts(c); + public List getNotRecordedChunkStepCounts() { + Cursor c = null; + try { + c = getReadableDatabase() + .query(TABLE_NAME, new String[]{COLUMN_DATE_AND_HOUR, COLUMN_STEPS}, + COLUMN_DATE_AND_HOUR + " != ? and " + + COLUMN_IS_RECORDED_ON_SERVER + " = ?", new String[]{"-1", "0"}, null, null, null); + Logger.log("Not recoreded Chunk Size: " + c.getCount()); + // cursor close is in method + return createChunkedStepCounts(c); + } catch (Exception e) { + e.printStackTrace(); + if (c != null) { + c.close(); + } + return new ArrayList<>(); + } } private List createChunkedStepCounts(Cursor c) { + if (c == null) { + return null; + } List lists = new ArrayList<>(); if (c.getCount() == 0) { @@ -338,37 +215,33 @@ private List createChunkedStepCounts(Cursor c) { while (c.moveToNext()) { lists.add(new ChunkStepCount(c.getLong(0), c.getInt(1))); } - c.close(); return lists; } - public void updateToRecorded(long[] dateAndHours) { - ContentValues values = new ContentValues(); - values.put(COLUMN_IS_RECORDED_ON_SERVER, "1"); - int length = dateAndHours.length; - String args = ""; - String[] dateAndHoursString = new String[length]; - for (int i = 0; i < length; i++) { - if (i == 0) { - args = "?"; - } else { - args += ", ?"; - } - dateAndHoursString[i] = String.valueOf(dateAndHours[i]); - } - SQLiteDatabase db = getWritableDatabase(); - db.beginTransaction(); + public void increaseByServerChunkStepCounts(List chunkStepCounts) { + SQLiteDatabase db = null; try { - int rows = db.update(TABLE_NAME, values, String.format(COLUMN_DATE_AND_HOUR + " in (%s)", args), dateAndHoursString); - Logger.log("updated number: " + rows); - logState(); + db = getWritableDatabase(); + db.beginTransaction(); + for (ChunkStepCount chunkStepCount : chunkStepCounts) { + increaseBySeverChunkStepCount(db, chunkStepCount); + } db.setTransactionSuccessful(); } finally { - db.endTransaction(); + if (db != null) { + db.endTransaction(); + } + } + } + + private void increaseBySeverChunkStepCount(SQLiteDatabase db, ChunkStepCount chunkStepCount) { + int currentSteps = getStepsImpl(db, chunkStepCount.unixTimeMillis); + if (chunkStepCount.steps >= currentSteps) { + insertOrReplaceStepRow(db, chunkStepCount.unixTimeMillis, chunkStepCount.steps, true); } + } - Logger.log("update to Recorded: " + dateAndHoursString.toString()); - db.close(); - return; + private void closeCursor(@Nullable Cursor cursor) { + if (cursor != null) cursor.close(); } } diff --git a/src/main/java/com/sukesan1984/stepsensorlib/PreferenceManager.java b/src/main/java/com/sukesan1984/stepsensorlib/PreferenceManager.java index a0d965e..b5c7b4f 100644 --- a/src/main/java/com/sukesan1984/stepsensorlib/PreferenceManager.java +++ b/src/main/java/com/sukesan1984/stepsensorlib/PreferenceManager.java @@ -4,11 +4,8 @@ import android.content.SharedPreferences; import android.support.annotation.NonNull; -/** - * Created by kosuketakami on 2017/01/16. - */ - -public class PreferenceManager { +// TODO: use this for service restart +class PreferenceManager { private static String PEDOMETER = "pedometer"; private static String CORRECT_SHUTDOWN = "corretctShutdown"; private static String STEPS_SINCE_BOOT = "stepsSinceBoot"; diff --git a/src/main/java/com/sukesan1984/stepsensorlib/SensorListener.java b/src/main/java/com/sukesan1984/stepsensorlib/SensorListener.java new file mode 100644 index 0000000..3e7cc12 --- /dev/null +++ b/src/main/java/com/sukesan1984/stepsensorlib/SensorListener.java @@ -0,0 +1,129 @@ +package com.sukesan1984.stepsensorlib; + +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.sukesan1984.stepsensorlib.util.Logger; + +public class SensorListener extends Service implements SensorEventListener { + + private final static int MICROSECONDS_IN_ONE_MINUTE = 60 * 1000 * 1000; + private final static String EXTRA_RESET_DATA = BuildConfig.APPLICATION_ID + ".ResetData"; + private static final int MAX_DELAY_MINUTES = 1; + + /** + * Start service, and write unsaved data to DB. + */ + static Intent createIntent(@NonNull Context context) { + return new Intent(context, SensorListener.class); + } + + public static Intent createIntentForReset(Context context) { + return new Intent(context, SensorListener.class).putExtra(EXTRA_RESET_DATA, true); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // nobody knows what happens here: step value might magically decrease + // when this method is called... + Logger.log(sensor.getName() + " accuracy changed: " + accuracy); + } + + @Override + public void onSensorChanged(SensorEvent event) { + int stepsSinceBoot = (int) event.values[0]; + StepCountCoordinator.getInstance().onStepCounterEvent(this, stepsSinceBoot); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + Logger.log("################################## onStartCommand called ####################################"); + if (intent != null && intent.getBooleanExtra(EXTRA_RESET_DATA, false)) { + Logger.log("Deleting all data and stopping service."); + unregisterSensor(); + StepCountCoordinator.getInstance().reset(); + Database.getInstance(this).deleteAll(); + stopSelf(); + return START_NOT_STICKY; + } + + // restart service every minutes get the current step count + ((AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)) + .set(AlarmManager.RTC, System.currentTimeMillis() + MAX_DELAY_MINUTES * 60 * 1000, + PendingIntent.getService(this, 2, createIntent(this), PendingIntent.FLAG_UPDATE_CURRENT)); + StepCountCoordinator.getInstance().saveSteps(this); + return START_STICKY; + } + + @Override + public void onCreate() { + super.onCreate(); + Logger.log("SensorListener onCreate"); + registerSensor(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + Logger.log("sensor service task removed"); + + // restart service + ((AlarmManager) getSystemService(Context.ALARM_SERVICE)) + .set(AlarmManager.RTC, System.currentTimeMillis() + 500, PendingIntent + .getService(this, 3, createIntent(this), 0)); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Logger.log("SensorListener onDestroy"); + unregisterSensor(); + } + + private void unregisterSensor() { + try { + SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); + sm.unregisterListener(this); + } catch (Exception e) { + if (BuildConfig.DEBUG) { + Logger.log(e); + e.printStackTrace(); + } + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private void registerSensor() { + Logger.log("register sensor listener"); + SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); + Logger.log("step sensors: " + sm.getSensorList(Sensor.TYPE_STEP_COUNTER).size()); + if (sm.getSensorList(Sensor.TYPE_STEP_COUNTER).size() < 1) { + return; + } + Logger.log("default: " + sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER).getName()); + + // enable batching with delay of max 5min + if (StepSensorFacade.isValidStepSensorDevice(this)) { + sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), + SensorManager.SENSOR_DELAY_NORMAL, MAX_DELAY_MINUTES * MICROSECONDS_IN_ONE_MINUTE); + } + } +} diff --git a/src/main/java/com/sukesan1984/stepsensorlib/ShutdownReceiver.java b/src/main/java/com/sukesan1984/stepsensorlib/ShutdownReceiver.java index ba1c52d..16eb2c2 100644 --- a/src/main/java/com/sukesan1984/stepsensorlib/ShutdownReceiver.java +++ b/src/main/java/com/sukesan1984/stepsensorlib/ShutdownReceiver.java @@ -5,30 +5,11 @@ import android.content.Intent; import com.sukesan1984.stepsensorlib.util.Logger; -import com.sukesan1984.stepsensorlib.util.SensorListener; - -/** - * Created by kosuketakami on 2016/11/05. - */ public class ShutdownReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (BuildConfig.DEBUG) Logger.log("shutting down"); - - context.startService(new Intent(context, SensorListener.class)); - - // if the user used a root script for shutdown, the DEVICE_SHUTDOWN - // broadcast might not be send. Thereforee, the app will check this - // setting on the next boot and displays an error message if it's not - // set to true - PreferenceManager.writeCorrectShutDown(context, true); - - Database db = Database.getInstance(context); - - db.resetLastUpdatedSteps(db.getLastUpdatedSteps()); - if (BuildConfig.DEBUG) db.logState(); - - db.close(); + StepCountCoordinator.getInstance().saveSteps(context); } } diff --git a/src/main/java/com/sukesan1984/stepsensorlib/StepCountCoordinator.java b/src/main/java/com/sukesan1984/stepsensorlib/StepCountCoordinator.java new file mode 100644 index 0000000..6e73916 --- /dev/null +++ b/src/main/java/com/sukesan1984/stepsensorlib/StepCountCoordinator.java @@ -0,0 +1,96 @@ +package com.sukesan1984.stepsensorlib; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.Log; +import com.sukesan1984.stepsensorlib.util.DateUtils; + +class StepCountCoordinator { + private static final String TAG = "StepCountCoordinator"; + private static final StepCountCoordinator singleton = new StepCountCoordinator(); + // https://finc.slack.com/archives/C3552EVFV/p1497343956278850 + // https://finc.slack.com/archives/C2AFVMQ5V/p1495508346662505 + private static final long MAX_STEPS_PER_HOUR = 18000; + + @Nullable + private Long dateAndHourOfLastEvent; + private int lastSteps; + private int unsavedSteps = 0; + + public static StepCountCoordinator getInstance() { + return singleton; + } + + private StepCountCoordinator() { + } + + public synchronized void onStepCounterEvent(Context context, int stepsSinceBoot) { + if (stepsSinceBoot < 0) { + Log.d(TAG, "onStepCounterEvent: Oops... stepsSinceBoot is negative... This SHOULD NOT happen..."); + return; + } + + long dateAndHour = DateUtils.getCurrentDateAndHour(); + if (dateAndHourOfLastEvent == null) { + dateAndHourOfLastEvent = dateAndHour; + lastSteps = stepsSinceBoot; + Log.v(TAG, "onStepCounterEvent: lastSteps is set to " + lastSteps); + return; + } + + if (dateAndHour != dateAndHourOfLastEvent) { + Log.d(TAG, "Save by hour change."); + saveSteps(context); + } + + int increment = stepsSinceBoot - lastSteps; + if (increment > MAX_STEPS_PER_HOUR) { + Log.d(TAG, "onStepCounterEvent: Skipping steps, increment " + increment + " exceeds limit of " + MAX_STEPS_PER_HOUR + "."); + lastSteps = stepsSinceBoot; + return; + } + if (increment < 0) { + Log.d(TAG, "onStepCounterEvent: Discarding steps event, negative increment " + + increment + " happened! (stepsSinceBoot: " + stepsSinceBoot + ")"); + // XXX: Resetting can cause extra steps to be added if the problem is order of event is randomized. + // But it can be sudden decrease of internal counter of sensor, then no step recorded + // until restarting the app or counter reaches lastSteps value. Counting extra steps is + // far more better than no steps, so I choose to reset here! + lastSteps = stepsSinceBoot; + return; + } + + unsavedSteps += increment; + Log.v(TAG, "onStepCounterEvent: " + unsavedSteps); + dateAndHourOfLastEvent = dateAndHour; + lastSteps = stepsSinceBoot; + } + + public synchronized void saveSteps(Context context) { + if (dateAndHourOfLastEvent == null) return; + Log.v(TAG, "saveSteps: " + unsavedSteps); + if (unsavedSteps == 0) return; + Database database = Database.getInstance(context); + int newSteps = database.addSteps(dateAndHourOfLastEvent, unsavedSteps); + if (newSteps < 0) { + Log.e(TAG, "Failed to save steps."); + } else { + unsavedSteps = 0; + } + } + + public synchronized int getTodaySteps(Context context) { + long dateAndHour = DateUtils.getCurrentDateAndHour(); + if (dateAndHourOfLastEvent != null && dateAndHour != dateAndHourOfLastEvent) { + // Save and flush step count before fetch. + saveSteps(context); + } + return Database.getInstance(context).getTodayStep() + unsavedSteps; + } + + public synchronized void reset() { + dateAndHourOfLastEvent = null; + lastSteps = 0; + unsavedSteps = 0; + } +} diff --git a/src/main/java/com/sukesan1984/stepsensorlib/StepSensorFacade.java b/src/main/java/com/sukesan1984/stepsensorlib/StepSensorFacade.java index 60c8202..7e995ae 100644 --- a/src/main/java/com/sukesan1984/stepsensorlib/StepSensorFacade.java +++ b/src/main/java/com/sukesan1984/stepsensorlib/StepSensorFacade.java @@ -3,15 +3,58 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; +import android.support.annotation.NonNull; + +import com.sukesan1984.stepsensorlib.model.ChunkStepCount; + +import java.util.ArrayList; +import java.util.List; /** * Step Sensor Facade */ public class StepSensorFacade { + private StepSensorFacade() { + throw new AssertionError(); + } + public static boolean isValidStepSensorDevice(Context context) { // TODO: get from server side black list model. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_STEP_COUNTER); } + + public static void startService(Context context) { + context.startService(SensorListener.createIntent(context)); + } + + public static void saveNow(Context context) { + StepCountCoordinator.getInstance().saveSteps(context); + } + + public static int getTodaySteps(Context context) { + return StepCountCoordinator.getInstance().getTodaySteps(context); + } + + public static void clearAllData(Context context) { + context.startService(SensorListener.createIntentForReset(context)); + } + + public static void increaseByServerChunkStepCounts(Context context, List chunkStepCounts) { + saveNow(context); + Database.getInstance(context).increaseByServerChunkStepCounts(chunkStepCounts); + } + + @NonNull + public static List getChunkStepsSince(Context context, long dateAndHour) { + saveNow(context); + return Database.getInstance(context).getChunkStepsSince(dateAndHour); + } + + @NonNull + public static List getNotRecordedChunkStepCounts(Context context) { + saveNow(context); + return Database.getInstance(context).getNotRecordedChunkStepCounts(); + } } diff --git a/src/main/java/com/sukesan1984/stepsensorlib/util/Logger.java b/src/main/java/com/sukesan1984/stepsensorlib/util/Logger.java index 4da95ad..2e21b00 100644 --- a/src/main/java/com/sukesan1984/stepsensorlib/util/Logger.java +++ b/src/main/java/com/sukesan1984/stepsensorlib/util/Logger.java @@ -15,11 +15,15 @@ */ package com.sukesan1984.stepsensorlib.util; +import android.app.ActivityManager; +import android.content.Context; import android.database.Cursor; +import com.crashlytics.android.Crashlytics; import com.sukesan1984.stepsensorlib.BuildConfig; import java.util.Date; +import java.util.List; public abstract class Logger { @@ -33,17 +37,62 @@ public static void log(Throwable ex) { } } + + private static String stackToString(StackTraceElement[] stackTrace) { + String stack = "Finc-log Stacktrace:"; + for (StackTraceElement stackTraceElement : stackTrace) { + stack += ",at " + (stackTraceElement.toString()); + } + return stack; + } + + public static void logInCrashlytics(Context context){ + Crashlytics.log("Thread id: " + Thread.currentThread().getId()); + Crashlytics.log("Process id: " + android.os.Process.myPid()); + Crashlytics.log("Process name: " + getProcessName(context)); + if (BuildConfig.DEBUG) { + Crashlytics.log("Stack: " + stackToString(Thread.currentThread().getStackTrace())); + } + } + + + private static String getProcessName(Context context) { + int pid = android.os.Process.myPid(); + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List infos = manager.getRunningAppProcesses(); + if (infos != null) { + for (ActivityManager.RunningAppProcessInfo processInfo : infos) { + if (processInfo.pid == pid) { + return processInfo.processName; + } + } + } + + return null; + } + public static void log(final Cursor c) { if (!BuildConfig.DEBUG) return; c.moveToFirst(); String title = ""; - for (int i = 0; i < c.getColumnCount(); i++) + for (int i = 0; i < c.getColumnCount(); i++) { title += c.getColumnName(i) + "\t| "; + } log(title); while (!c.isAfterLast()) { title = ""; - for (int i = 0; i < c.getColumnCount(); i++) - title += c.getString(i) + "\t| "; + for (int i = 0; i < c.getColumnCount(); i++) { + final int columnTitleLength = c.getColumnName(i).length(); + int columnValueLength = c.getString(i).length(); + int diffLength = columnTitleLength - columnValueLength > 0 + ? columnTitleLength - columnValueLength + : 0; + String diff = ""; + for (int diffCount = 0; diffCount < diffLength; diffCount++) { + diff += " "; + } + title += c.getString(i) + diff + "\t| "; + } log(title); c.moveToNext(); } diff --git a/src/main/java/com/sukesan1984/stepsensorlib/util/SensorListener.java b/src/main/java/com/sukesan1984/stepsensorlib/util/SensorListener.java deleted file mode 100644 index a468176..0000000 --- a/src/main/java/com/sukesan1984/stepsensorlib/util/SensorListener.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.sukesan1984.stepsensorlib.util; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.IBinder; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import com.sukesan1984.stepsensorlib.BuildConfig; -import com.sukesan1984.stepsensorlib.Database; -import com.sukesan1984.stepsensorlib.PreferenceManager; -import com.sukesan1984.stepsensorlib.StepSensorFacade; - -/** - * Created by kosuketakami on 2016/11/05. - */ - -public class SensorListener extends Service implements SensorEventListener { - - public final static String ACTION_PAUSE = "pause"; - private static int steps; - - private static boolean WAIT_FOR_VALID_STEPS = false; - - private final static int MICROSECONDS_IN_ONE_MINUTE = 60000000; - - public static Intent createIntent(@NonNull Context context) { - return new Intent(context, SensorListener.class); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // nobody knows what happens here: step value might magically decrease - // when this method is called... - if (BuildConfig.DEBUG) { - Logger.log(sensor.getName() + " accuracy changed: " + accuracy); - } - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.values[0] > Integer.MAX_VALUE) { - if (BuildConfig.DEBUG) { - Logger.log("probably not a real value: " + event.values[0]); - return; - } - } else { - steps = (int) event.values[0]; - if (WAIT_FOR_VALID_STEPS && steps > 0) { - Logger.log("periodically save"); - WAIT_FOR_VALID_STEPS = false; - Database db = Database.getInstance(this); - db.updateOrInsert(DateUtils.getCurrentDateAndHour(), steps); - reRegisterSensor(); - int difference = steps - PreferenceManager.readStepsSinceBoot(this, steps); - if (difference > 0) { - PreferenceManager.writeStepsSinceBoot(this, steps); - } - db.close(); - } - } - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log.d("Hoge", "################################## onStartCommand"); - if (intent != null && ACTION_PAUSE.equals(intent.getStringExtra("action"))) { - if (BuildConfig.DEBUG) { - Logger.log("onStartCommand action: " + intent.getStringExtra("action")); - } - if (steps == 0) { - Database db = Database.getInstance(this); - steps = db.getLastUpdatedSteps(); - db.close(); - } - SharedPreferences prefs = getSharedPreferences("pedometer", Context.MODE_PRIVATE); - ((AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)) - .cancel(PendingIntent.getService(getApplicationContext(), 2, - new Intent(this, SensorListener.class), - PendingIntent.FLAG_UPDATE_CURRENT)); - stopSelf(); - return START_NOT_STICKY; - } - - // restart service every minutes get the current step count - ((AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)) - .set(AlarmManager.RTC, System.currentTimeMillis() + 60 * 1000, - PendingIntent.getService(getApplicationContext(), 2, new Intent(this, SensorListener.class), - PendingIntent.FLAG_UPDATE_CURRENT)); - WAIT_FOR_VALID_STEPS = true; - - return START_STICKY; - } - - @Override - public void onCreate() { - super.onCreate(); - if (BuildConfig.DEBUG) { - Logger.log("SensorListener onCreate"); - } - reRegisterSensor(); - } - - @Override - public void onTaskRemoved(Intent rootIntent) { - super.onTaskRemoved(rootIntent); - if (BuildConfig.DEBUG) { - Logger.log("sensor service task removed"); - } - - ((AlarmManager) getSystemService(Context.ALARM_SERVICE)) - .set(AlarmManager.RTC, System.currentTimeMillis() + 500, PendingIntent - .getService(this, 3, new Intent(this, SensorListener.class), 0)); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) { - Logger.log("SensorListener onDestroy"); - try { - SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); - sm.unregisterListener(this); - } catch (Exception e) { - if (BuildConfig.DEBUG) { - Logger.log(e); - e.printStackTrace(); - } - } - } - } - - private void reRegisterSensor() { - if (BuildConfig.DEBUG) { - Logger.log("re-register sensor listener"); - } - SensorManager sm = (SensorManager) getSystemService(SENSOR_SERVICE); - try { - sm.unregisterListener(this); - } catch (Exception e) { - if (BuildConfig.DEBUG) { - Logger.log(e); - e.printStackTrace(); - } - } - - if (BuildConfig.DEBUG) { - Logger.log("step sensors: " + sm.getSensorList(Sensor.TYPE_STEP_COUNTER).size()); - if (sm.getSensorList(Sensor.TYPE_STEP_COUNTER).size() < 1) { - return; - } - Logger.log("default: " + sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER).getName()); - } - - // enable batching with delay of max 5min - if (StepSensorFacade.isValidStepSensorDevice(this)) { - sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), - SensorManager.SENSOR_DELAY_NORMAL, 5 * MICROSECONDS_IN_ONE_MINUTE); - } - } -}