diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5edb4ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..98a40e8 Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7ac24c7 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2ce9f65 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..91cdbae --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "lk.paradox.kekayan.fabfit" + minSdkVersion 19 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation 'com.android.support:support-vector-drawable:27.1.1' + implementation 'com.android.support:support-v4:27.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'com.github.blackfizz:eazegraph:1.2.5l@aar' + implementation 'com.nineoldandroids:library:2.4.0' + implementation 'com.google.android.apps.dashclock:dashclock-api:2.0.0' + implementation 'com.github.j4velin.colorpicker:colorpicker:1.20.6' + implementation 'com.twitter.sdk.android:twitter:3.1.1' + implementation 'com.squareup.okhttp3:okhttp:3.9.0' + implementation 'com.squareup.picasso:picasso:2.5.2' + +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/lk/paradox/kekayan/fabfit/ExampleInstrumentedTest.java b/app/src/androidTest/java/lk/paradox/kekayan/fabfit/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3bd0bb5 --- /dev/null +++ b/app/src/androidTest/java/lk/paradox/kekayan/fabfit/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package lk.paradox.kekayan.fabfit; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("lk.paradox.kekayan.fabfit", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9ef1b85 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/MainActivity.java b/app/src/main/java/lk/paradox/kekayan/fabfit/MainActivity.java new file mode 100644 index 0000000..611ea36 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/MainActivity.java @@ -0,0 +1,99 @@ +package lk.paradox.kekayan.fabfit; + + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.BottomNavigationView; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AppCompatActivity; +import android.view.MenuItem; +import android.widget.TextView; +import android.widget.Toast; + +import lk.paradox.kekayan.fabfit.fragments.ProfileFragment; +import lk.paradox.kekayan.fabfit.fragments.StepsFragment; +import lk.paradox.kekayan.fabfit.fragments.TweetsFragment; +import lk.paradox.kekayan.fabfit.sensors.SensorListener; + +public class MainActivity extends AppCompatActivity { + + private static final int TIME_INTERVAL = 2000; + private TextView mTextMessage; + private long mBackPressed; + private FragmentManager fm; + private FragmentTransaction ft; + //click events for bottom navigation buttons + private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener + = new BottomNavigationView.OnNavigationItemSelectedListener() { + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.navigation_home: + StepsFragment stepsFragment = new StepsFragment(); + getSupportFragmentManager().beginTransaction() + .replace(R.id.frame_container, stepsFragment) + .addToBackStack(null) + .commit(); + + return true; + case R.id.navigation_tweets: + TweetsFragment tweetsFragment = new TweetsFragment(); + getSupportFragmentManager().beginTransaction() + .replace(R.id.frame_container, tweetsFragment) + .addToBackStack(null) + .commit(); + return true; + case R.id.navigation_settings: + ProfileFragment profileFragment = new ProfileFragment(); + getSupportFragmentManager().beginTransaction() + .replace(R.id.frame_container, profileFragment) + .addToBackStack(null) + .commit(); + return true; + } + return false; + } + }; + + @Override + public void onBackPressed() { + + fm = getSupportFragmentManager(); + ft = fm.beginTransaction(); + if (fm.getBackStackEntryCount() > 0) { + fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); + ft.commit(); + } else if (mBackPressed + TIME_INTERVAL > System.currentTimeMillis()) { + super.onBackPressed(); + } else { + Toast.makeText(this, "Press again to exit the app", Toast.LENGTH_SHORT).show(); + } + mBackPressed = System.currentTimeMillis(); + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + //start the sensorlistener service + startService(new Intent(this, SensorListener.class)); + //loading the stepsfragment as main when app launched + FragmentManager fragmentManager = getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.frame_container); + + if (fragment == null) { + fragment = new StepsFragment(); + fragmentManager.beginTransaction().add(R.id.frame_container, fragment).commit(); + } + + BottomNavigationView navigation = findViewById(R.id.navigation); + navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener); + } + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/SplashActivity.java b/app/src/main/java/lk/paradox/kekayan/fabfit/SplashActivity.java new file mode 100644 index 0000000..c2063b6 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/SplashActivity.java @@ -0,0 +1,20 @@ +package lk.paradox.kekayan.fabfit; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +public class SplashActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + //specify splash screen’s background as the activity’s theme background + //So we can avoid blank white page appears during splash launching + //we do not have setContentView() for this SplashActivity. + // View is displaying from the theme and this way it is faster than creating a layout + Intent intent = new Intent(this, MainActivity.class); + startActivity(intent); + finish(); + } +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/db/Database.java b/app/src/main/java/lk/paradox/kekayan/fabfit/db/Database.java new file mode 100644 index 0000000..e49821f --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/db/Database.java @@ -0,0 +1,370 @@ +package lk.paradox.kekayan.fabfit.db; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import lk.paradox.kekayan.fabfit.BuildConfig; +import lk.paradox.kekayan.fabfit.helpers.Logger; +import lk.paradox.kekayan.fabfit.helpers.Util; + + +public class Database extends SQLiteOpenHelper { + + private final static String DB_NAME = "steps"; + private final static int DB_VERSION = 2; + private static final AtomicInteger openCounter = new AtomicInteger(); + private static Database instance; + + private Database(final Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + 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(final SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + DB_NAME + " (date INTEGER, steps INTEGER)"); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 1) { + // drop PRIMARY KEY constraint + db.execSQL("CREATE TABLE " + DB_NAME + "2 (date INTEGER, steps INTEGER)"); + db.execSQL("INSERT INTO " + DB_NAME + "2 (date, steps) SELECT date, steps FROM " + + DB_NAME); + db.execSQL("DROP TABLE " + DB_NAME); + db.execSQL("ALTER TABLE " + DB_NAME + "2 RENAME TO " + DB_NAME + ""); + } + } + + /** + * 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(DB_NAME, columns, selection, selectionArgs, groupBy, having, orderBy, limit); + } + + /** + * Inserts a new entry in the database, if there is no entry for the given + * date yet. Steps should be the current number of steps and it's negative + * value will be used as offset for the new date. Also adds 'steps' steps to + * the previous day, if there is an entry for that date. + *

+ * This method does nothing if there is already an entry for 'date' - use + * {@link } in this case. + *

+ * To restore data from a backup, use {@link #insertDayFromBackup} + * + * @param date the date in ms since 1970 + * @param steps the current step value to be used as negative offset for the + * new day; must be >= 0 + */ + public void insertNewDay(long date, int steps) { + getWritableDatabase().beginTransaction(); + try { + Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"date"}, "date = ?", + new String[]{String.valueOf(date)}, null, null, null); + if (c.getCount() == 0 && steps >= 0) { + + // add 'steps' to yesterdays count + addToLastEntry(steps); + + // add today + ContentValues values = new ContentValues(); + values.put("date", date); + // use the negative steps as offset + values.put("steps", -steps); + getWritableDatabase().insert(DB_NAME, null, values); + } + c.close(); + if (BuildConfig.DEBUG) { + Logger.log("insertDay " + date + " / " + steps); + logState(); + } + getWritableDatabase().setTransactionSuccessful(); + } finally { + getWritableDatabase().endTransaction(); + } + } + + /** + * Adds the given number of steps to the last entry in the database + * + * @param steps the number of steps to add. Must be > 0 + */ + public void addToLastEntry(int steps) { + if (steps > 0) { + getWritableDatabase().execSQL("UPDATE " + DB_NAME + " SET steps = steps + " + steps + + " WHERE date = (SELECT MAX(date) FROM " + DB_NAME + ")"); + } + } + + /** + * Inserts a new entry in the database, overwriting any existing entry for the given date. + * Use this method for restoring data from a backup. + * + * @param date the date in ms since 1970 + * @param steps the step value for 'date'; must be >= 0 + * @return true if a new entry was created, false if there was already an + * entry for 'date' (and it was overwritten) + */ + public boolean insertDayFromBackup(long date, int steps) { + getWritableDatabase().beginTransaction(); + boolean newEntryCreated = false; + try { + ContentValues values = new ContentValues(); + values.put("steps", steps); + int updatedRows = getWritableDatabase() + .update(DB_NAME, values, "date = ?", new String[]{String.valueOf(date)}); + if (updatedRows == 0) { + values.put("date", date); + getWritableDatabase().insert(DB_NAME, null, values); + newEntryCreated = true; + } + getWritableDatabase().setTransactionSuccessful(); + } finally { + getWritableDatabase().endTransaction(); + } + return newEntryCreated; + } + + /** + * Writes the current steps database to the log + */ + public void logState() { + if (BuildConfig.DEBUG) { + Cursor c = getReadableDatabase() + .query(DB_NAME, null, null, null, null, null, "date DESC", "5"); + Logger.log(c); + c.close(); + } + } + + /** + * Get the total of steps taken without today's value + * + * @return number of steps taken, ignoring today + */ + public int getTotalWithoutToday() { + Cursor c = getReadableDatabase() + .query(DB_NAME, new String[]{"SUM(steps)"}, "steps > 0 AND date > 0 AND date < ?", + new String[]{String.valueOf(Util.getToday())}, null, null, null); + c.moveToFirst(); + int re = c.getInt(0); + c.close(); + return re; + } + + /** + * Get the maximum of steps walked in one day + * + * @return the maximum number of steps walked in one day + */ + public int getRecord() { + Cursor c = getReadableDatabase() + .query(DB_NAME, new String[]{"MAX(steps)"}, "date > 0", null, null, null, null); + c.moveToFirst(); + int re = c.getInt(0); + c.close(); + return re; + } + + /** + * Get the maximum of steps walked in one day and the date that happend + * + * @return a pair containing the date (Date) in millis since 1970 and the + * step value (Integer) + */ + public Pair getRecordData() { + Cursor c = getReadableDatabase() + .query(DB_NAME, new String[]{"date, steps"}, "date > 0", null, null, null, + "steps DESC", "1"); + c.moveToFirst(); + Pair p = new Pair(new Date(c.getLong(0)), c.getInt(1)); + c.close(); + return p; + } + + /** + * Get the number of steps taken for a specific date. + *

+ * If date is Util.getToday(), this method returns the offset which needs to + * be added to the value returned by getCurrentSteps() to get todays steps. + * + * @param date the date in millis since 1970 + * @return the steps taken on this date or Integer.MIN_VALUE if date doesn't + * exist in the database + */ + public int getSteps(final long date) { + Cursor c = getReadableDatabase().query(DB_NAME, new String[]{"steps"}, "date = ?", + new String[]{String.valueOf(date)}, null, null, null); + c.moveToFirst(); + int re; + if (c.getCount() == 0) re = Integer.MIN_VALUE; + else re = c.getInt(0); + c.close(); + return re; + } + + /** + * Gets the last num entries in descending order of date (newest first) + * + * @param num the number of entries to get + * @return a list of long,integer pair - the first being the date, the second the number of steps + */ + public List> getLastEntries(int num) { + Cursor c = getReadableDatabase() + .query(DB_NAME, new String[]{"date", "steps"}, "date > 0", null, null, null, + "date DESC", String.valueOf(num)); + int max = c.getCount(); + List> result = new ArrayList<>(max); + if (c.moveToFirst()) { + do { + result.add(new Pair<>(c.getLong(0), c.getInt(1))); + } while (c.moveToNext()); + } + return result; + } + + /** + * Get the number of steps taken between 'start' and 'end' date + *

+ * Note that todays entry might have a negative value, so take care of that + * if 'end' >= Util.getToday()! + * + * @param start start date in ms since 1970 (steps for this date included) + * @param end end date in ms since 1970 (steps for this date included) + * @return the number of steps from 'start' to 'end'. Can be < 0 as todays + * entry might have negative value + */ + public int getSteps(final long start, final long end) { + Cursor c = getReadableDatabase() + .query(DB_NAME, new String[]{"SUM(steps)"}, "date >= ? AND date <= ?", + new String[]{String.valueOf(start), String.valueOf(end)}, null, null, null); + int re; + if (c.getCount() == 0) { + re = 0; + } else { + c.moveToFirst(); + re = c.getInt(0); + } + c.close(); + return re; + } + + /** + * 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 + */ + public void removeNegativeEntries() { + getWritableDatabase().delete(DB_NAME, "steps < ?", new String[]{"0"}); + } + + /** + * Removes invalid entries from the database. + *

+ * Currently, an invalid input is such with steps >= 200,000 + */ + public void removeInvalidEntries() { + getWritableDatabase().delete(DB_NAME, "steps >= ?", new String[]{"200000"}); + } + + /** + * Get the number of 'valid' days (= days with a step value > 0). + *

+ * The current day is not added to this number. + * + * @return the number of days with a step value > 0, return will be >= 0 + */ + public int getDaysWithoutToday() { + Cursor c = getReadableDatabase() + .query(DB_NAME, new String[]{"COUNT(*)"}, "steps > ? AND date < ? AND date > 0", + new String[]{String.valueOf(0), String.valueOf(Util.getToday())}, null, + null, null); + c.moveToFirst(); + int re = c.getInt(0); + c.close(); + return re < 0 ? 0 : re; + } + + /** + * Get the number of 'valid' days (= days with a step value > 0). + *

+ * The current day is also added to this number, even if the value in the + * database might still be < 0. + *

+ * It is safe to divide by the return value as this will be at least 1 (and + * not 0). + * + * @return the number of days with a step value > 0, return will be >= 1 + */ + public int getDays() { + // todays is not counted yet + int re = this.getDaysWithoutToday() + 1; + return re; + } + + /** + * Saves the current 'steps since boot' sensor value in the database. + * + * @param steps since boot + */ + public void saveCurrentSteps(int steps) { + ContentValues values = new ContentValues(); + values.put("steps", steps); + if (getWritableDatabase().update(DB_NAME, values, "date = -1", null) == 0) { + values.put("date", -1); + getWritableDatabase().insert(DB_NAME, null, values); + } + if (BuildConfig.DEBUG) { + Logger.log("saving steps in db: " + steps); + } + } + + /** + * Reads the latest saved value for the 'steps since boot' sensor value. + * + * @return the current number of steps saved in the database or 0 if there + * is no entry + */ + public int getCurrentSteps() { + int re = getSteps(-1); + return re == Integer.MIN_VALUE ? 0 : re; + } +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/ProfileFragment.java b/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/ProfileFragment.java new file mode 100644 index 0000000..1a31f91 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/ProfileFragment.java @@ -0,0 +1,22 @@ +package lk.paradox.kekayan.fabfit.fragments; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import lk.paradox.kekayan.fabfit.R; + +public class ProfileFragment extends Fragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_profile, container, false); + return view; + } + + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/SettingsFragment.java b/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/SettingsFragment.java new file mode 100644 index 0000000..eac69be --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/SettingsFragment.java @@ -0,0 +1,29 @@ +package lk.paradox.kekayan.fabfit.fragments; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Locale; + +import lk.paradox.kekayan.fabfit.R; + + +public class SettingsFragment extends Fragment { + final static int DEFAULT_WEIGHT = 52; + final static int DEFAULT_GOAL = 10000; + final static float DEFAULT_STEP_SIZE = Locale.getDefault() == Locale.US ? 2.5f : 75f; + final static String DEFAULT_STEP_UNIT = Locale.getDefault() == Locale.US ? "ft" : "cm"; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_settings, container, false); + } + + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/StepsFragment.java b/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/StepsFragment.java new file mode 100644 index 0000000..bd7f50d --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/fragments/StepsFragment.java @@ -0,0 +1,265 @@ +package lk.paradox.kekayan.fabfit.fragments; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.eazegraph.lib.charts.PieChart; +import org.eazegraph.lib.models.PieModel; + +import java.text.NumberFormat; +import java.util.Locale; +import java.util.Objects; + +import lk.paradox.kekayan.fabfit.BuildConfig; +import lk.paradox.kekayan.fabfit.R; +import lk.paradox.kekayan.fabfit.db.Database; +import lk.paradox.kekayan.fabfit.helpers.Logger; +import lk.paradox.kekayan.fabfit.helpers.Util; + + +public class StepsFragment extends Fragment implements SensorEventListener { + + public final static NumberFormat formatter = NumberFormat.getInstance(Locale.getDefault()); + + ImageView footImage; + private TextView stepsView, totalView, averageView, caloriesView; + private PieModel sliceGoal, sliceCurrent; + private PieChart pg; + private int todayOffset, total_start, goal, since_boot, total_days; + private boolean showSteps = true; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View v = inflater.inflate(R.layout.fragment_steps, null); + + footImage = v.findViewById(R.id.stepsimageView); + + stepsView = v.findViewById(R.id.steps); + totalView = v.findViewById(R.id.total); + averageView = v.findViewById(R.id.average); + caloriesView = v.findViewById(R.id.tv_calories); + + pg = v.findViewById(R.id.graph); + pg.setInnerPadding(88.f); + // slice for the steps taken today + sliceCurrent = new PieModel("", 0, Color.parseColor("#99CC00")); + pg.addPieSlice(sliceCurrent); + + // slice for the "missing" steps until reaching the goal + sliceGoal = new PieModel("", SettingsFragment.DEFAULT_GOAL, Color.parseColor("#CC0000")); + pg.addPieSlice(sliceGoal); + + pg.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View view) { + showSteps = !showSteps; + stepsDistanceChanged(); + } + }); + pg.startAnimation(); + return v; + } + + @Override + public void onResume() { + super.onResume(); + + + Database db = Database.getInstance(getActivity()); + + if (BuildConfig.DEBUG) db.logState(); + // read todays offset + todayOffset = db.getSteps(Util.getToday()); + + SharedPreferences prefs = + Objects.requireNonNull(getActivity()).getSharedPreferences("FabFit", Context.MODE_PRIVATE); + + goal = prefs.getInt("goal", SettingsFragment.DEFAULT_GOAL); + since_boot = db.getCurrentSteps(); // do not use the value from the sharedPreferences + int pauseDifference = since_boot - prefs.getInt("pauseCount", since_boot); + + // register a sensorlistener to live update the UI if a step is taken + if (!prefs.contains("pauseCount")) { + SensorManager sm = + (SensorManager) getActivity().getSystemService(Context.SENSOR_SERVICE); + Sensor sensor = sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER); + if (sensor == null) { + new AlertDialog.Builder(getActivity()).setTitle(R.string.no_sensor) + .setMessage(R.string.no_sensor_explain) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialogInterface) { + getActivity().finish(); + } + }).setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + } + }).create().show(); + } else { + sm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI, 0); + } + } + + since_boot -= pauseDifference; + + total_start = db.getTotalWithoutToday(); + total_days = db.getDays(); + + db.close(); + + stepsDistanceChanged(); + } + + /** + * Call this method if the Fragment should update the "steps"/"km" text in + * the pie graph as well as the pie and the bars graphs. + */ + private void stepsDistanceChanged() { + if (showSteps) { + getView().findViewById(R.id.stepsimageView).setVisibility(View.VISIBLE); + getView().findViewById(R.id.unit).setVisibility(View.GONE); + ((TextView) getView().findViewById(R.id.unit)).setText("Steps"); + + } else { + String unit = getActivity().getSharedPreferences("FabFit", Context.MODE_PRIVATE) + .getString("stepsize_unit", SettingsFragment.DEFAULT_STEP_UNIT); + if (unit.equals("cm")) { + unit = "km"; + } else { + unit = "mi"; + } + getView().findViewById(R.id.stepsimageView).setVisibility(View.GONE); + ((TextView) getView().findViewById(R.id.unit)).setText(unit); + getView().findViewById(R.id.unit).setVisibility(View.VISIBLE); + } + + updatePie(); + } + + @Override + public void onPause() { + super.onPause(); + try { + SensorManager sm = + (SensorManager) getActivity().getSystemService(Context.SENSOR_SERVICE); + sm.unregisterListener(this); + } catch (Exception e) { + e.printStackTrace(); + } + Database db = Database.getInstance(getActivity()); + db.saveCurrentSteps(since_boot); + db.close(); + } + + + @Override + public void onAccuracyChanged(final Sensor sensor, int accuracy) { + // won't happen + } + + @Override + public void onSensorChanged(final SensorEvent event) { + if (BuildConfig.DEBUG) + Logger.log("UI - sensorChanged | todayOffset: " + todayOffset + " since boot: " + + event.values[0]); + if (event.values[0] > Integer.MAX_VALUE || event.values[0] == 0) { + return; + } + if (todayOffset == Integer.MIN_VALUE) { + // no values for today + // we dont know when the reboot was, so set todays steps to 0 by + // initializing them with -STEPS_SINCE_BOOT + todayOffset = -(int) event.values[0]; + Database db = Database.getInstance(getActivity()); + db.insertNewDay(Util.getToday(), (int) event.values[0]); + db.close(); + } + since_boot = (int) event.values[0]; + updatePie(); + } + + /** + * Updates the pie graph to show todays steps/distance as well as the + * yesterday and total values. Should be called when switching from step + * count to distance. + */ + private void updatePie() { + if (BuildConfig.DEBUG) Logger.log("UI - update steps: " + since_boot); + // todayOffset might still be Integer.MIN_VALUE on first start + int steps_today = Math.max(todayOffset + since_boot, 0); + sliceCurrent.setValue(steps_today); + if (goal - steps_today > 0) { + // goal not reached yet + if (pg.getData().size() == 1) { + // can happen if the goal value was changed: old goal value was + // reached but now there are some steps missing for the new goal + pg.addPieSlice(sliceGoal); + } + sliceGoal.setValue(goal - steps_today); + } else { + // goal reached + pg.clearChart(); + pg.addPieSlice(sliceCurrent); + } + pg.update(); + if (showSteps) { + stepsView.setText(formatter.format(steps_today)); + totalView.setText(formatter.format(total_start + steps_today)); + averageView.setText(formatter.format((total_start + steps_today) / total_days)); + caloriesView.setText(formatter.format(calculateCalories(steps_today))); + } else { + // update only every 10 steps when displaying distance + SharedPreferences prefs = + Objects.requireNonNull(getActivity()).getSharedPreferences("FabFit", Context.MODE_PRIVATE); + float stepsize = prefs.getFloat("stepsize_value", SettingsFragment.DEFAULT_STEP_SIZE); + float distance_today = steps_today * stepsize; + float distance_total = (total_start + steps_today) * stepsize; + if (prefs.getString("stepsize_unit", SettingsFragment.DEFAULT_STEP_UNIT) + .equals("cm")) { + distance_today /= 100000; + distance_total /= 100000; + } else { + distance_today /= 5280; + distance_total /= 5280; + } + + stepsView.setText(formatter.format(distance_today)); + totalView.setText(formatter.format(distance_total)); + averageView.setText(formatter.format(distance_total / total_days)); + caloriesView.setText(formatter.format(calculateCalories(steps_today))); + + } + } + + public double calculateCalories(int stepscount) { + + d return mCalories; + } + + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/helpers/Logger.java b/app/src/main/java/lk/paradox/kekayan/fabfit/helpers/Logger.java new file mode 100644 index 0000000..34bea03 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/helpers/Logger.java @@ -0,0 +1,69 @@ +package lk.paradox.kekayan.fabfit.helpers; + +import android.database.Cursor; +import android.os.Environment; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Date; + +import lk.paradox.kekayan.fabfit.BuildConfig; + +public abstract class Logger { + + private static final Date date = new Date(); + private final static String APP = "FabFit"; + private static FileWriter fw; + + public static void log(Throwable ex) { + log(ex.getMessage()); + for (StackTraceElement ste : ex.getStackTrace()) { + log(ste.toString()); + } + } + + public static void log(final Cursor c) { + if (!BuildConfig.DEBUG) return; + c.moveToFirst(); + String title = ""; + 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| "; + log(title); + c.moveToNext(); + } + } + + @SuppressWarnings("deprecation") + public static void log(String msg) { + if (!BuildConfig.DEBUG) return; + android.util.Log.d(APP, msg); + try { + if (fw == null) { + fw = new FileWriter(new File( + Environment.getExternalStorageDirectory().toString() + "/" + APP + ".log"), + true); + } + date.setTime(System.currentTimeMillis()); + fw.write(date.toLocaleString() + " - " + msg + "\n"); + fw.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + // } + } + + protected void finalize() throws Throwable { + try { + if (fw != null) fw.close(); + } finally { + super.finalize(); + } + } + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/helpers/Util.java b/app/src/main/java/lk/paradox/kekayan/fabfit/helpers/Util.java new file mode 100644 index 0000000..af4ed0d --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/helpers/Util.java @@ -0,0 +1,33 @@ +package lk.paradox.kekayan.fabfit.helpers; + +import java.util.Calendar; + +public abstract class Util { + + /** + * @return milliseconds since 1.1.1970 for today 0:00:00 local timezone + */ + public static long getToday() { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(System.currentTimeMillis()); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + + /** + * @return milliseconds since 1.1.1970 for tomorrow 0:00:01 local timezone + */ + public static long getTomorrow() { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(System.currentTimeMillis()); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 1); + c.set(Calendar.MILLISECOND, 0); + c.add(Calendar.DATE, 1); + return c.getTimeInMillis(); + } +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/BootReceiver.java b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/BootReceiver.java new file mode 100644 index 0000000..6df379f --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/BootReceiver.java @@ -0,0 +1,38 @@ +package lk.paradox.kekayan.fabfit.sensors; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import lk.paradox.kekayan.fabfit.BuildConfig; +import lk.paradox.kekayan.fabfit.db.Database; +import lk.paradox.kekayan.fabfit.helpers.Logger; + +public class BootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(final Context context, final Intent intent) { + if (BuildConfig.DEBUG) Logger.log("booted"); + + SharedPreferences prefs = context.getSharedPreferences("FabFit", Context.MODE_PRIVATE); + + Database db = Database.getInstance(context); + + if (!prefs.getBoolean("correctShutdown", false)) { + if (BuildConfig.DEBUG) Logger.log("Incorrect shutdown"); + // can we at least recover some steps? + int steps = db.getCurrentSteps(); + if (BuildConfig.DEBUG) Logger.log("Trying to recover " + steps + " steps"); + db.addToLastEntry(steps); + } + // last entry might still have a negative step value, so remove that + // row if that's the case + db.removeNegativeEntries(); + db.saveCurrentSteps(0); + db.close(); + prefs.edit().remove("correctShutdown").apply(); + + context.startService(new Intent(context, SensorListener.class)); + } +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/PowerReceiver.java b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/PowerReceiver.java new file mode 100644 index 0000000..7949113 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/PowerReceiver.java @@ -0,0 +1,25 @@ +package lk.paradox.kekayan.fabfit.sensors; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +public class PowerReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, final Intent intent) { + SharedPreferences prefs = + context.getSharedPreferences("FabFit", Context.MODE_MULTI_PROCESS); + if (Intent.ACTION_POWER_CONNECTED.equals(intent.getAction()) && + !prefs.contains("pauseCount")) { + // if power connected & not already paused, then pause now + context.startService(new Intent(context, SensorListener.class) + .putExtra("action", SensorListener.ACTION_PAUSE)); + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(intent.getAction()) && + prefs.contains("pauseCount")) { + // if power disconnected & currently paused, then resume now + context.startService(new Intent(context, SensorListener.class) + .putExtra("action", SensorListener.ACTION_PAUSE)); + } + } +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/SensorListener.java b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/SensorListener.java new file mode 100644 index 0000000..41b9b0d --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/SensorListener.java @@ -0,0 +1,242 @@ +package lk.paradox.kekayan.fabfit.sensors; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +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 java.text.NumberFormat; +import java.util.Date; +import java.util.Locale; + +import lk.paradox.kekayan.fabfit.BuildConfig; +import lk.paradox.kekayan.fabfit.MainActivity; +import lk.paradox.kekayan.fabfit.R; +import lk.paradox.kekayan.fabfit.db.Database; +import lk.paradox.kekayan.fabfit.helpers.Logger; +import lk.paradox.kekayan.fabfit.helpers.Util; +import lk.paradox.kekayan.fabfit.widget.WidgetUpdateService; + +/** + * Background service which keeps the step-sensor listener alive to always get + * the number of steps since boot. + *

+ * This service won't be needed any more if there is a way to read the + * step-value without waiting for a sensor event + */ +public class SensorListener extends Service implements SensorEventListener { + + public final static String ACTION_PAUSE = "pause"; + public final static String ACTION_UPDATE_NOTIFICATION = "updateNotificationState"; + private final static int NOTIFICATION_ID = 1; + private final static long MICROSECONDS_IN_ONE_MINUTE = 60000000; + private final static long SAVE_OFFSET_TIME = AlarmManager.INTERVAL_HOUR; + private final static int SAVE_OFFSET_STEPS = 500; + private static int steps; + private static int lastSaveSteps; + private static long lastSaveTime; + + @Override + public void onAccuracyChanged(final 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(final 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]; + updateIfNecessary(); + } + } + + private void updateIfNecessary() { + if (steps > lastSaveSteps + SAVE_OFFSET_STEPS || + (steps > 0 && System.currentTimeMillis() > lastSaveTime + SAVE_OFFSET_TIME)) { + if (BuildConfig.DEBUG) Logger.log( + "saving steps: steps=" + steps + " lastSave=" + lastSaveSteps + + " lastSaveTime=" + new Date(lastSaveTime)); + Database db = Database.getInstance(this); + if (db.getSteps(Util.getToday()) == Integer.MIN_VALUE) { + int pauseDifference = steps - + getSharedPreferences("FabFit", Context.MODE_PRIVATE) + .getInt("pauseCount", steps); + db.insertNewDay(Util.getToday(), steps - pauseDifference); + if (pauseDifference > 0) { + // update pauseCount for the new day + getSharedPreferences("FabFit", Context.MODE_PRIVATE).edit() + .putInt("pauseCount", steps).apply(); + } + } + db.saveCurrentSteps(steps); + db.close(); + lastSaveSteps = steps; + lastSaveTime = System.currentTimeMillis(); + updateNotificationState(); + startService(new Intent(this, WidgetUpdateService.class)); + } + } + + @Override + public IBinder onBind(final Intent intent) { + return null; + } + + @Override + public int onStartCommand(final Intent intent, int flags, int startId) { + 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.getCurrentSteps(); + db.close(); + } + SharedPreferences prefs = getSharedPreferences("FabFit", Context.MODE_PRIVATE); + if (prefs.contains("pauseCount")) { // resume counting + int difference = steps - + prefs.getInt("pauseCount", steps); // number of steps taken during the pause + Database db = Database.getInstance(this); + db.addToLastEntry(-difference); + db.close(); + prefs.edit().remove("pauseCount").apply(); + updateNotificationState(); + } else { // pause counting + // cancel restart + ((AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)) + .cancel(PendingIntent.getService(getApplicationContext(), 2, + new Intent(this, SensorListener.class), + PendingIntent.FLAG_UPDATE_CURRENT)); + prefs.edit().putInt("pauseCount", steps).apply(); + updateNotificationState(); + stopSelf(); + return START_NOT_STICKY; + } + } + + if (intent != null && intent.getBooleanExtra(ACTION_UPDATE_NOTIFICATION, false)) { + updateNotificationState(); + } else { + updateIfNecessary(); + } + + // restart service every hour to save the current step count + ((AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE)) + .set(AlarmManager.RTC, Math.min(Util.getTomorrow(), + System.currentTimeMillis() + AlarmManager.INTERVAL_HOUR), PendingIntent + .getService(getApplicationContext(), 2, + new Intent(this, SensorListener.class), + PendingIntent.FLAG_UPDATE_CURRENT)); + + return START_STICKY; + } + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) Logger.log("SensorListener onCreate"); + reRegisterSensor(); + updateNotificationState(); + } + + @Override + public void onTaskRemoved(final Intent rootIntent) { + super.onTaskRemoved(rootIntent); + if (BuildConfig.DEBUG) Logger.log("sensor service task removed"); + // Restart service in 500 ms + ((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 updateNotificationState() { + if (BuildConfig.DEBUG) Logger.log("SensorListener updateNotificationState"); + SharedPreferences prefs = getSharedPreferences("FabFit", Context.MODE_PRIVATE); + NotificationManager nm = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (prefs.getBoolean("notification", true)) { + int goal = prefs.getInt("goal", 10000); + Database db = Database.getInstance(this); + int today_offset = db.getSteps(Util.getToday()); + if (steps == 0) + steps = db.getCurrentSteps(); // use saved value if we haven't anything better + db.close(); + Notification.Builder notificationBuilder = new Notification.Builder(this); + if (steps > 0) { + if (today_offset == Integer.MIN_VALUE) today_offset = -steps; + notificationBuilder.setProgress(goal, today_offset + steps, false).setContentText( + today_offset + steps >= goal ? getString(R.string.goal_reached_notification, + NumberFormat.getInstance(Locale.getDefault()) + .format((today_offset + steps))) : + getString(R.string.notification_text, + NumberFormat.getInstance(Locale.getDefault()) + .format((goal - today_offset - steps)))); + } else { // still no step value? + notificationBuilder + .setContentText(getString(R.string.your_progress_will_be_shown_here_soon)); + } + boolean isPaused = prefs.contains("pauseCount"); + notificationBuilder.setPriority(Notification.PRIORITY_MIN).setShowWhen(false) + .setContentTitle(isPaused ? getString(R.string.ispaused) : + getString(R.string.notification_title)).setContentIntent(PendingIntent + .getActivity(this, 0, new Intent(this, MainActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT)) + .setSmallIcon(R.drawable.ic_notification) + .addAction(isPaused ? R.drawable.ic_resume : R.drawable.ic_pause, + isPaused ? getString(R.string.resume) : getString(R.string.pause), + PendingIntent.getService(this, 4, new Intent(this, SensorListener.class) + .putExtra("action", ACTION_PAUSE), + PendingIntent.FLAG_UPDATE_CURRENT)).setOngoing(true); + nm.notify(NOTIFICATION_ID, notificationBuilder.build()); + } else { + nm.cancel(NOTIFICATION_ID); + } + } + + 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; // emulator + Logger.log("default: " + sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER).getName()); + } + + // enable batching with delay of max 5 min + sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), + SensorManager.SENSOR_DELAY_NORMAL, (int) (5 * MICROSECONDS_IN_ONE_MINUTE)); + } +} \ No newline at end of file diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/ShutdownRecevier.java b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/ShutdownRecevier.java new file mode 100644 index 0000000..b8d210b --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/sensors/ShutdownRecevier.java @@ -0,0 +1,47 @@ +package lk.paradox.kekayan.fabfit.sensors; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import lk.paradox.kekayan.fabfit.BuildConfig; +import lk.paradox.kekayan.fabfit.db.Database; +import lk.paradox.kekayan.fabfit.helpers.Logger; +import lk.paradox.kekayan.fabfit.helpers.Util; + +public class ShutdownRecevier extends BroadcastReceiver { + + @Override + public void onReceive(final Context context, final 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. Therefore, the app will check this + // setting on the next boot and displays an error message if it's not + // set to true + context.getSharedPreferences("FabFit", Context.MODE_PRIVATE).edit() + .putBoolean("correctShutdown", true).apply(); + + Database db = Database.getInstance(context); + // if it's already a new day, add the temp. steps to the last one + if (db.getSteps(Util.getToday()) == Integer.MIN_VALUE) { + int steps = db.getCurrentSteps(); + int pauseDifference = steps - + context.getSharedPreferences("FabFit", Context.MODE_PRIVATE) + .getInt("pauseCount", steps); + db.insertNewDay(Util.getToday(), steps - pauseDifference); + if (pauseDifference > 0) { + // update pauseCount for the new day + context.getSharedPreferences("FabFit", Context.MODE_PRIVATE).edit() + .putInt("pauseCount", steps).apply(); + } + } else { + db.addToLastEntry(db.getCurrentSteps()); + } + // current steps will be reset on boot @see BootReceiver + db.close(); + } + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/widget/DashClock.java b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/DashClock.java new file mode 100644 index 0000000..3e2b66e --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/DashClock.java @@ -0,0 +1,32 @@ +package lk.paradox.kekayan.fabfit.widget; + +import android.content.Intent; + +import com.google.android.apps.dashclock.api.DashClockExtension; +import com.google.android.apps.dashclock.api.ExtensionData; + +import lk.paradox.kekayan.fabfit.MainActivity; +import lk.paradox.kekayan.fabfit.R; +import lk.paradox.kekayan.fabfit.db.Database; +import lk.paradox.kekayan.fabfit.fragments.StepsFragment; +import lk.paradox.kekayan.fabfit.helpers.Util; + +/** + * Class for providing a DashClock (https://code.google.com/p/dashclock) + * extension + */ +public class DashClock extends DashClockExtension { + + @Override + protected void onUpdateData(int reason) { + ExtensionData data = new ExtensionData(); + Database db = Database.getInstance(this); + int steps = Math.max(db.getCurrentSteps() + db.getSteps(Util.getToday()), 0); + data.visible(true).status(StepsFragment.formatter.format(steps)) + .icon(R.drawable.ic_dashclock) + .clickIntent(new Intent(DashClock.this, MainActivity.class)); + db.close(); + publishUpdate(data); + } + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/widget/Widget.java b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/Widget.java new file mode 100644 index 0000000..f53f843 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/Widget.java @@ -0,0 +1,38 @@ +package lk.paradox.kekayan.fabfit.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.widget.RemoteViews; + +import lk.paradox.kekayan.fabfit.MainActivity; +import lk.paradox.kekayan.fabfit.R; + +public class Widget extends AppWidgetProvider { + + static RemoteViews updateWidget(final int appWidgetId, final Context context, final int steps) { + final SharedPreferences prefs = context.getSharedPreferences("Widgets", + Context.MODE_PRIVATE); + final PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, + new Intent(context, MainActivity.class), 0); + + final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget); + views.setOnClickPendingIntent(R.id.widget, pendingIntent); + views.setTextColor(R.id.widgetsteps, prefs.getInt("color_" + appWidgetId, Color.WHITE)); + views.setTextViewText(R.id.widgetsteps, String.valueOf(steps)); + views.setTextColor(R.id.widgettext, prefs.getInt("color_" + appWidgetId, Color.WHITE)); + views.setInt(R.id.widget, "setBackgroundColor", + prefs.getInt("background_" + appWidgetId, Color.TRANSPARENT)); + return views; + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + context.startService(new Intent(context, WidgetUpdateService.class)); + } + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/widget/WidgetConfig.java b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/WidgetConfig.java new file mode 100644 index 0000000..d146639 --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/WidgetConfig.java @@ -0,0 +1,88 @@ +/* + * Copyright 2014 Thomas Hoffmann + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lk.paradox.kekayan.fabfit.widget; + +import android.app.Activity; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; + +import de.j4velin.lib.colorpicker.ColorPickerDialog; +import de.j4velin.lib.colorpicker.ColorPreviewButton; +import lk.paradox.kekayan.fabfit.R; + +public class WidgetConfig extends Activity implements OnClickListener { + + private static int widgetId; + + @Override + protected void onPause() { + super.onPause(); + startService(new Intent(this, WidgetUpdateService.class)); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent intent = getIntent(); + final Bundle extras = intent.getExtras(); + if (extras != null) { + setContentView(R.layout.widgetconfig); + + ColorPreviewButton textcolor = findViewById(R.id.textcolor); + textcolor.setOnClickListener(this); + textcolor.setColor(Color.WHITE); + ColorPreviewButton bgcolor = findViewById(R.id.bgcolor); + bgcolor.setOnClickListener(this); + bgcolor.setColor(Color.TRANSPARENT); + + widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + + final Intent resultValue = new Intent(); + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); + setResult(RESULT_OK, resultValue); + } else { + finish(); + } + } + + @Override + public void onClick(final View v) { + ColorPickerDialog dialog = new ColorPickerDialog(this, + (findViewById(v.getId()).getTag() != null) ? + (Integer) findViewById(v.getId()).getTag() : -1); + dialog.setHexValueEnabled(true); + dialog.setAlphaSliderVisible(true); + dialog.setOnColorChangedListener(new ColorPickerDialog.OnColorChangedListener() { + @Override + public void onColorChanged(int color) { + ((ColorPreviewButton) v).setColor(color); + v.setTag(color); + getSharedPreferences("Widgets", Context.MODE_PRIVATE).edit() + .putInt((v.getId() == R.id.bgcolor ? "background_" : "color_") + widgetId, + color).apply(); + } + }); + dialog.show(); + } + +} diff --git a/app/src/main/java/lk/paradox/kekayan/fabfit/widget/WidgetUpdateService.java b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/WidgetUpdateService.java new file mode 100644 index 0000000..411baaa --- /dev/null +++ b/app/src/main/java/lk/paradox/kekayan/fabfit/widget/WidgetUpdateService.java @@ -0,0 +1,33 @@ +package lk.paradox.kekayan.fabfit.widget; + +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Intent; +import android.os.IBinder; + +import lk.paradox.kekayan.fabfit.db.Database; +import lk.paradox.kekayan.fabfit.helpers.Util; + +public class WidgetUpdateService extends Service { + + @Override + public IBinder onBind(final Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Database db = Database.getInstance(this); + int steps = Math.max(db.getCurrentSteps() + db.getSteps(Util.getToday()), 0); + db.close(); + final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(WidgetUpdateService.this); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(WidgetUpdateService.this, Widget.class)); + for (int appWidgetId : appWidgetIds) { + appWidgetManager.updateAppWidget(appWidgetId, Widget.updateWidget(appWidgetId, WidgetUpdateService.this, steps)); + } + stopSelf(); + return START_NOT_STICKY; + } + +} diff --git a/app/src/main/res/drawable-hdpi/fabfitlogo.png b/app/src/main/res/drawable-hdpi/fabfitlogo.png new file mode 100644 index 0000000..2b5301f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/fabfitlogo.png differ diff --git a/app/src/main/res/drawable-hdpi/footicon.png b/app/src/main/res/drawable-hdpi/footicon.png new file mode 100644 index 0000000..7ed3ed7 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/footicon.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_dashclock.png b/app/src/main/res/drawable-hdpi/ic_dashclock.png new file mode 100644 index 0000000..2348803 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_dashclock.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..991832a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_pause.png b/app/src/main/res/drawable-hdpi/ic_pause.png new file mode 100644 index 0000000..307bf26 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_pause.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_resume.png b/app/src/main/res/drawable-hdpi/ic_resume.png new file mode 100644 index 0000000..6384a4e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_resume.png differ diff --git a/app/src/main/res/drawable-mdpi/fabfitlogo.png b/app/src/main/res/drawable-mdpi/fabfitlogo.png new file mode 100644 index 0000000..21f4da4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/fabfitlogo.png differ diff --git a/app/src/main/res/drawable-mdpi/footicon.png b/app/src/main/res/drawable-mdpi/footicon.png new file mode 100644 index 0000000..4151439 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/footicon.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_dashclock.png b/app/src/main/res/drawable-mdpi/ic_dashclock.png new file mode 100644 index 0000000..ae07882 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_dashclock.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..85956e3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_pause.png b/app/src/main/res/drawable-mdpi/ic_pause.png new file mode 100644 index 0000000..89a6735 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_pause.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_resume.png b/app/src/main/res/drawable-mdpi/ic_resume.png new file mode 100644 index 0000000..4552ae6 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_resume.png differ diff --git a/app/src/main/res/drawable-v21/background.xml b/app/src/main/res/drawable-v21/background.xml new file mode 100644 index 0000000..e00212f --- /dev/null +++ b/app/src/main/res/drawable-v21/background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/fabfitlogo.png b/app/src/main/res/drawable-xhdpi/fabfitlogo.png new file mode 100644 index 0000000..68e3562 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/fabfitlogo.png differ diff --git a/app/src/main/res/drawable-xhdpi/footicon.png b/app/src/main/res/drawable-xhdpi/footicon.png new file mode 100644 index 0000000..735ce2b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/footicon.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_dashclock.png b/app/src/main/res/drawable-xhdpi/ic_dashclock.png new file mode 100644 index 0000000..5f58071 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_dashclock.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..f5fa4cc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_pause.png b/app/src/main/res/drawable-xhdpi/ic_pause.png new file mode 100644 index 0000000..ab20740 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_pause.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_resume.png b/app/src/main/res/drawable-xhdpi/ic_resume.png new file mode 100644 index 0000000..652ac68 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_resume.png differ diff --git a/app/src/main/res/drawable-xxhdpi/fabfitlogo.png b/app/src/main/res/drawable-xxhdpi/fabfitlogo.png new file mode 100644 index 0000000..3e0d830 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/fabfitlogo.png differ diff --git a/app/src/main/res/drawable-xxhdpi/footicon.png b/app/src/main/res/drawable-xxhdpi/footicon.png new file mode 100644 index 0000000..ac5ee29 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/footicon.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_dashclock.png b/app/src/main/res/drawable-xxhdpi/ic_dashclock.png new file mode 100644 index 0000000..318bb55 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_dashclock.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..741e767 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause.png b/app/src/main/res/drawable-xxhdpi/ic_pause.png new file mode 100644 index 0000000..c457d4e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_pause.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_resume.png b/app/src/main/res/drawable-xxhdpi/ic_resume.png new file mode 100644 index 0000000..14293ac Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_resume.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/fabfitlogo.png b/app/src/main/res/drawable-xxxhdpi/fabfitlogo.png new file mode 100644 index 0000000..ae5f60a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/fabfitlogo.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/footicon.png b/app/src/main/res/drawable-xxxhdpi/footicon.png new file mode 100644 index 0000000..ca2d9b5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/footicon.png differ diff --git a/app/src/main/res/drawable/background.xml b/app/src/main/res/drawable/background.xml new file mode 100644 index 0000000..e00212f --- /dev/null +++ b/app/src/main/res/drawable/background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_gradient.xml b/app/src/main/res/drawable/bg_gradient.xml new file mode 100644 index 0000000..2a94bc9 --- /dev/null +++ b/app/src/main/res/drawable/bg_gradient.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..ae6a446 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 0000000..0c36320 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 0000000..94cae9c --- /dev/null +++ b/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..6d0c6e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_walk.xml b/app/src/main/res/drawable/ic_walk.xml new file mode 100644 index 0000000..cf8e38b --- /dev/null +++ b/app/src/main/res/drawable/ic_walk.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/splash_bg.xml b/app/src/main/res/drawable/splash_bg.xml new file mode 100644 index 0000000..70313f7 --- /dev/null +++ b/app/src/main/res/drawable/splash_bg.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml new file mode 100644 index 0000000..e8d9549 --- /dev/null +++ b/app/src/main/res/layout/about.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..fe699d5 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..dcce73f --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000..b158645 --- /dev/null +++ b/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..453d34f --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_steps.xml b/app/src/main/res/layout/fragment_steps.xml new file mode 100644 index 0000000..027f87c --- /dev/null +++ b/app/src/main/res/layout/fragment_steps.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_tweets.xml b/app/src/main/res/layout/fragment_tweets.xml new file mode 100644 index 0000000..b2f41e1 --- /dev/null +++ b/app/src/main/res/layout/fragment_tweets.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget.xml b/app/src/main/res/layout/widget.xml new file mode 100644 index 0000000..c4a472c --- /dev/null +++ b/app/src/main/res/layout/widget.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widgetconfig.xml b/app/src/main/res/layout/widgetconfig.xml new file mode 100644 index 0000000..1110105 --- /dev/null +++ b/app/src/main/res/layout/widgetconfig.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/navigation.xml b/app/src/main/res/menu/navigation.xml new file mode 100644 index 0000000..ead5198 --- /dev/null +++ b/app/src/main/res/menu/navigation.xml @@ -0,0 +1,23 @@ + +

+ + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a2f5908 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8ca12fe Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c7cba40 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #141E30 + #243B55 + #FF4081 + #ffffff + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..59f0483 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,35 @@ + + FabFit + Steps + Tweets + Settings + Sensor not found + This app requires a dedicated hardware step sensor - which your device does not have. This app won\'t run on your device. + No permission to access external storage + + Hello blank fragment + Hello blank fragment1 + Hello blank fragment2 + Goal reached! %s steps and counting + FabFit is counting + %s steps to go + Resume + Pause + FabFit is paused + Your progress will be shown here soon + Text color: + Background color: + Profile + + No55ivtC0J45mPdDjdxMcv82n + peLL4vdtUiWXiOKSDAnwqpCzBe3P2lxLr787sdO1ffxp2GXQwD + Calories + 10.000 + stepicon + steps + total + average + Loading Tweets + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..356cdee --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/app/src/main/res/xml/widget.xml b/app/src/main/res/xml/widget.xml new file mode 100644 index 0000000..8ea9a07 --- /dev/null +++ b/app/src/main/res/xml/widget.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/test/java/lk/paradox/kekayan/fabfit/ExampleUnitTest.java b/app/src/test/java/lk/paradox/kekayan/fabfit/ExampleUnitTest.java new file mode 100644 index 0000000..878bfa8 --- /dev/null +++ b/app/src/test/java/lk/paradox/kekayan/fabfit/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package lk.paradox.kekayan.fabfit; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f140e1e --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.2.0-alpha15' + + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..743d692 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7a3265e Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2287b7b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri May 18 19:33:08 IST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'