diff --git a/ActivityMonitorService.java b/ActivityMonitorService.java new file mode 100644 index 00000000..d0e1e3a7 --- /dev/null +++ b/ActivityMonitorService.java @@ -0,0 +1,226 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.app.NotificationChannel; +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.Build; +import android.os.IBinder; +import android.os.PowerManager; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import java.util.Calendar; + + +/** + * This class is a service that runs in the background and handles tracking and storing of + * movement to avoid losing track if Actifit is running instead + */ + +public class ActivityMonitorService extends Service implements SensorEventListener, StepListener { + + + private StepsDBHelper mStepsDBHelper; + private Sensor accSensor; + public static SensorManager sensorManager; + private SimpleStepDetector simpleStepDetector; + private int notificationID = 1; + private NotificationCompat.Builder mBuilder; + private NotificationManagerCompat notificationManager; + + private SharedPreferences sharedPreferences; + private static PowerManager pm; + private static PowerManager.WakeLock wl; + + public static PowerManager getPowerManagerInstance(){ + return pm; + } + + public static PowerManager.WakeLock getWakeLockInstance(){ + return wl; + } + + public ActivityMonitorService(Context applicationContext) { + super(); + Log.d(MainActivity.TAG,">>>>[Actifit]here I am!"); + } + + public ActivityMonitorService() { + + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createNotificationChannel() { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = getString(R.string.actifit_notif_channel); + String description = getString(R.string.actifit_notif_description); + //making a fix for no sound in Android 8 + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel channel = new NotificationChannel( + getString(R.string.actifit_channel_ID), name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + + @Override + public void onCreate(){ + + + //initialize power manager and wake locks either way + pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ACTIFIT:ACTIFIT_SPECIAL_LOCK"); + + //check if aggressive background tracking mode is enabled + + sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + String aggModeEnabled = sharedPreferences.getString("aggressiveBackgroundTracking" + ,getString(R.string.aggr_back_tracking_off)); + + //enable wake lock to ensure tracking functions in the background + if (aggModeEnabled.equals(getString(R.string.aggr_back_tracking_on))) { + Log.d(MainActivity.TAG,">>>>[Actifit]AGG MODE ON"); + wl.acquire(); + } + + mStepsDBHelper = new StepsDBHelper(this); + // Get an instance of the SensorManager + sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); + + accSensor = + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + + //for Android 8, need to initialize notifications first + createNotificationChannel(); + + //initiate step detector and start tracking + simpleStepDetector = new SimpleStepDetector(); + simpleStepDetector.registerListener(this); + + sensorManager.registerListener(ActivityMonitorService.this, accSensor, + SensorManager.SENSOR_DELAY_FASTEST); + + } + + /** + * this is overriding the step function which only works in case of using accelerometer sensor + * + * @param timeNs + */ + @Override + public void step(long timeNs) { + int curStepCount = mStepsDBHelper.createStepsEntry(); + + //adjust step count display and print to notification activity + //making sure we have an instance of mBuilder + if (mBuilder!=null) { + mBuilder.setContentText(getString(R.string.activity_today_string) + " " + curStepCount); + notificationManager.notify(notificationID, mBuilder.build()); + } + + //also update main activity + Intent in = new Intent(); + in.putExtra("move_count",curStepCount); + in.setAction("ACTIFIT_SERVICE"); + //sendBroadcast(in); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(in); + + //stepDisplay.setText(TEXT_NUM_STEPS + (curStepCount < 0 ? 0 : curStepCount)); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + /** + * handles the actual counting of steps relying on one of the two sensors, + * whichever is active + * + * @param event + */ + @Override + public void onSensorChanged(SensorEvent event) { + + simpleStepDetector.updateAccel( + event.timestamp, event.values[0], event.values[1], event.values[2]); + + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + + //grab activity count so far today + int curActivityCount = mStepsDBHelper.fetchTodayStepCount(); + + //create the service that will display as a notification on screen lock + Intent notificationIntent = new Intent(this, MainActivity.class); + + PendingIntent pendingIntent = + PendingIntent.getActivity(this, 0, notificationIntent, 0); + + mBuilder = new + NotificationCompat.Builder(this, getString(R.string.actifit_channel_ID)) + .setContentTitle("Actifit Report") + .setContentText(getString(R.string.activity_today_string)+" "+(curActivityCount<0?0:curActivityCount)) + .setSmallIcon(R.drawable.actifit_logo) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOnlyAlertOnce(true); + + notificationManager = NotificationManagerCompat.from(this); + + startForeground(notificationID,mBuilder.build()); + + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(MainActivity.TAG,">>>>[Actifit]ondestroy service!"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(Service.STOP_FOREGROUND_REMOVE); + } + sensorManager.unregisterListener(ActivityMonitorService.this); + + String aggModeEnabled = sharedPreferences.getString("aggressiveBackgroundTracking" + ,getString(R.string.aggr_back_tracking_off)); + + //release wake lock now + if (aggModeEnabled.equals(getString(R.string.aggr_back_tracking_on))) { + Log.d(MainActivity.TAG,">>>>[Actifit]AGG MODE ON - RELEASING LOCK"); + pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ACTIFIT:ACTIFIT_SPECIAL_LOCK"); + if (wl.isHeld()) { + wl.release(); + } + } + //service destroyed, let's start it again + //Intent broadcastIntent = new Intent(".MonitorRestart"); + // sendBroadcast(broadcastIntent); + + } + +} diff --git a/Charity.java b/Charity.java new file mode 100644 index 00000000..79aa2153 --- /dev/null +++ b/Charity.java @@ -0,0 +1,36 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +/** + * Class handles storage and rendering of relevant charities + */ +public class Charity { + + private String charityName; + private String displayName; + + public Charity(String charityName, String displayName) { + this.charityName = charityName; + this.displayName = displayName; + } + + public String getCharityName() { + return charityName; + } + + public void setCharityName(String charityName) { + this.charityName = charityName; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String toString() + { + return getDisplayName(); + } +} diff --git a/DateStepsModel.java b/DateStepsModel.java new file mode 100644 index 00000000..2f8547db --- /dev/null +++ b/DateStepsModel.java @@ -0,0 +1,8 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +public class DateStepsModel { + + public String mDate; + public int mStepCount; + public String mtrackingDevice; +} \ No newline at end of file diff --git a/HistoryChartActivity.java b/HistoryChartActivity.java new file mode 100644 index 00000000..0ed825d5 --- /dev/null +++ b/HistoryChartActivity.java @@ -0,0 +1,149 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.graphics.Color; +import android.os.Bundle; +import android.support.constraint.ConstraintLayout; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.RelativeLayout; + +import com.github.mikephil.charting.charts.BarChart; +import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.Description; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +import com.github.mikephil.charting.formatter.IAxisValueFormatter; + +import java.sql.Array; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +public class HistoryChartActivity extends AppCompatActivity { + + private StepsDBHelper mStepsDBHelper; + private ArrayList mStepCountList; + private ArrayList mStepFinalList; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_history_chart); + + mStepFinalList = new ArrayList(); + + //grab the data to be displayed in the list + getDataForList(); + + //initializing date conversion components + String dateDisplay; + //existing date format + SimpleDateFormat dateFormIn = new SimpleDateFormat("yyyyMMdd"); + //output format + SimpleDateFormat dateFormOut = new SimpleDateFormat("MM/dd/yyyy"); + + //loop through the data to prepare it for proper display + for (int position = 0; position < mStepCountList.size(); position++) { + try { + //grab date entry according to stored format + Date feedingDate = dateFormIn.parse((mStepCountList.get(position)).mDate); + //convert it to new format for display + dateDisplay = dateFormOut.format(feedingDate); + //append to display + String displayEntryTxt = dateDisplay + " - Total Activity: " + String.valueOf((mStepCountList.get(position)).mStepCount); + //append to display + if (mStepCountList.get(position).mtrackingDevice!=null && !mStepCountList.get(position).mtrackingDevice.equals("") + && !mStepCountList.get(position).mtrackingDevice.equals(StepsDBHelper.DEVICE_SENSORS)){ + displayEntryTxt += " ( "+mStepCountList.get(position).mtrackingDevice+" )"; + } + mStepFinalList.add(displayEntryTxt); + } catch (ParseException txtEx) { + Log.d(MainActivity.TAG,txtEx.toString()); + txtEx.printStackTrace(); + } + } + //reverse the list for descending display + Collections.reverse(mStepFinalList); + + //connect to the chart and fill it with data + BarChart chart = findViewById(R.id.activity_chart); + + List entries = new ArrayList(); + + final String[] labels = new String[mStepCountList.size()]; + + int data_id = 0; + //int data_id_int = 0; + for (DateStepsModel data : mStepCountList) { + labels[data_id] = ""+data.mDate; + entries.add(new BarEntry(data_id, Float.parseFloat(""+data.mStepCount))); + data_id+=1f; + //data_id_int++; + } + + BarDataSet dataSet = new BarDataSet(entries, "Activity Count"); + + BarData barData = new BarData( dataSet); + // set custom bar width + barData.setBarWidth(0.3f); + + + //customize X-axis + + IAxisValueFormatter formatter = new IAxisValueFormatter() { + + @Override + public String getFormattedValue(float value, AxisBase axis) { + return labels[(int) value]; + } + + }; + + XAxis xAxis = chart.getXAxis(); + xAxis.setGranularity(0.5f); // minimum axis-step (interval) + xAxis.setValueFormatter(formatter); + + //add limit lines to show marker of min 5K activity + YAxis yAxis = chart.getAxisLeft(); + + LimitLine line = new LimitLine(5000, "Min Reward - 5K Activity"); + line.setLineColor(Color.RED); + line.setLineWidth(4f); + line.setTextColor(Color.BLACK); + line.setTextSize(12f); + + yAxis.addLimitLine(line); + + //description field of chart + Description chartDescription = new Description(); + chartDescription.setText("Activity History Chart"); + chart.setDescription(chartDescription); + + //fill chart with data + chart.setData(barData); + + + // make the x-axis fit exactly all bars + //chart.setFitBars(true); + + //display data + chart.invalidate(); + } + + /** + * function handles preparing the proper data to the mStepCountList ArrayList + */ + public void getDataForList() { + mStepsDBHelper = new StepsDBHelper(this); + mStepCountList = mStepsDBHelper.readStepsEntries(); + } +} diff --git a/HttpResultHelper.java b/HttpResultHelper.java new file mode 100644 index 00000000..a44d50c5 --- /dev/null +++ b/HttpResultHelper.java @@ -0,0 +1,109 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import android.util.Base64; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.util.ArrayList; + +/** + * Class has been built based on provided code at https://notes.iopush.net/android-send-a-https-post-request/ + */ +public class HttpResultHelper { + private int statusCode; + private InputStream response; + + public HttpResultHelper() { + } + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public InputStream getResponse() { + return response; + } + + public void setResponse(InputStream response) { + this.response = response; + } + + public HttpResultHelper httpPost(String urlStr, String user, String password, String data, ArrayList headers, int timeOut) throws IOException + { + // Set url + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + // If secure connection + if (urlStr.startsWith("https")) { + try { + SSLContext sc; + sc = SSLContext.getInstance("TLS"); + sc.init(null, null, new java.security.SecureRandom()); + ((HttpsURLConnection)conn).setSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception e) { + Log.d(MainActivity.TAG,"Failed to construct SSL object" + e.getMessage()); + } + } + + + // Use this if you need basic authentication + if ((user != null) && (password != null)) { + String userPass = user + ":" + password; + String basicAuth = "Basic " + Base64.encodeToString(userPass.getBytes(), Base64.DEFAULT); + conn.setRequestProperty("Authorization", basicAuth); + } + + // Set Timeout and method + conn.setReadTimeout(timeOut); + conn.setConnectTimeout(timeOut); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + if (headers != null) { + for (int i = 0; i < headers.size(); i++) { + conn.setRequestProperty(headers.get(i)[0], headers.get(i)[1]); + } + } + + if (data != null) { + conn.setFixedLengthStreamingMode(data.getBytes().length); + OutputStream os = conn.getOutputStream(); + BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(os, "UTF-8")); + writer.write(data); + writer.flush(); + writer.close(); + os.close(); + } + + InputStream inputStream = null; + try + { + inputStream = conn.getInputStream(); + } + catch(IOException exception) + { + inputStream = conn.getErrorStream(); + } + + HttpResultHelper result = new HttpResultHelper(); + result.setStatusCode(conn.getResponseCode()); + result.setResponse(inputStream); + + return result; + } +} \ No newline at end of file diff --git a/LeaderboardActivity.java b/LeaderboardActivity.java new file mode 100644 index 00000000..6d2d7b58 --- /dev/null +++ b/LeaderboardActivity.java @@ -0,0 +1,196 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public class LeaderboardActivity extends AppCompatActivity { + + private ListView mAccountsListView; + private ArrayList mAccountsFinalList ; + private Context leadership_post_context; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_leaderboard); + + mAccountsListView = findViewById(R.id.accounts_list); + mAccountsFinalList = new ArrayList(); + + this.leadership_post_context = this; + final Activity currentActivity = this; + + //connect to the server via a thread to prevent application hangup + //and grab the data to be displayed in the list + new LeaderboardActivity.LeaderboardDataRequest(leadership_post_context, currentActivity).execute(); + } + + /** + * Class handles fetching the top 5 actifit account as a thread + */ + + private class LeaderboardDataRequest extends AsyncTask { + ProgressDialog progress; + private final Context context; + private Activity currentActivity; + private String notification; + + public LeaderboardDataRequest(Context c, Activity currentActivity){ + this.context = c; + this.currentActivity = currentActivity; + } + + protected void onPreExecute(){ + //create a new progress dialog to show action is underway + progress = new ProgressDialog(this.context); + progress.setMessage(getString(R.string.fetching_leaderboard)); + progress.show(); + } + protected Void doInBackground(String... params) { + try { + + String inputLine; + + String result = ""; + //connect to our leaderboard API + String urlStr = getString(R.string.leaderboard_url); + if (!getString(R.string.test_mode).equals("off")){ + urlStr = getString(R.string.test_leaderboard_url); + } + + // Headers + ArrayList headers = new ArrayList<>(); + + headers.add(new String[]{"Content-Type", "application/json"}); + + HttpResultHelper httpResult = new HttpResultHelper(); + + httpResult = httpResult.httpPost(urlStr, null, null, "", headers, 20000); + BufferedReader in = new BufferedReader(new InputStreamReader(httpResult.getResponse())); + while ((inputLine = in.readLine()) != null) { + result += inputLine; + } + + Log.d(MainActivity.TAG,">>>test:"+result); + + //check result of action + if (result.equals("zero")){ + notification = getString(R.string.leader_no_results); + + //display proper notification + displayNotification(notification, progress, context, currentActivity, result); + }else if (result.equals("error")){ + notification = getString(R.string.leader_error); + + //display proper notification + displayNotification(notification, progress, context, currentActivity, result); + }else{ + final String api_outcome = result; + currentActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + //hide the progressDialog + try { + if (progress != null && progress.isShowing()) { + progress.dismiss(); + } + }catch(Exception e){ + e.printStackTrace(); + } + + //need to render the result + List items = Arrays.asList(api_outcome.split(";")); + mAccountsFinalList.addAll(items); + + ArrayAdapter arrayAdapter = + new ArrayAdapter(currentActivity, android.R.layout.simple_list_item_1, mAccountsFinalList); + + mAccountsListView.setAdapter(arrayAdapter); + } + }); + } + + + }catch (Exception e){ + + //display proper notification + notification = getString(R.string.leader_error); + displayNotification(notification, progress, context, currentActivity, "error"); + + Log.d(MainActivity.TAG,"Error connecting:"+e.getMessage()); + e.printStackTrace(); + } + return null; + } + + } + void displayNotification(final String notification, final ProgressDialog progress, + final Context context, final Activity currentActivity, + final String success){ + //render result + currentActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + //hide the progressDialog + try{ + if (progress != null && progress.isShowing()) { + progress.dismiss(); + } + }catch(Exception e){ + e.printStackTrace(); + } + /*spinner=findViewById(R.id.progressBar); + spinner.setVisibility(View.GONE);*/ + + AlertDialog.Builder builder1 = new AlertDialog.Builder(context); + builder1.setMessage(notification); + + builder1.setCancelable(true); + + builder1.setPositiveButton( + "Dismiss", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + if (success.equals("zero") || success.equals("error")) { + //close current screen + Log.d(MainActivity.TAG,">>>Finish"); + currentActivity.finish(); + } + } + }); + //create and display alert window + try { + AlertDialog alert11 = builder1.create(); + alert11.show(); + }catch(Exception e){ + Log.d(MainActivity.TAG,"Error creating dialog"+e.getMessage()); + } + } + }); + + //finish(); + } +} diff --git a/MainActivity.java b/MainActivity.java new file mode 100644 index 00000000..99f2e6bb --- /dev/null +++ b/MainActivity.java @@ -0,0 +1,690 @@ +/* + * Copyright 2013 Google Inc. + * + * 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 io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.Handler; +import android.os.IBinder; +import android.os.PowerManager; +import android.provider.MediaStore; +import android.support.annotation.RequiresApi; +import android.support.customtabs.CustomTabsIntent; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.FileProvider; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.hardware.SensorEventListener; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.scottyab.rootbeer.RootBeer; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.Scanner; +import java.util.UUID; + +import static android.os.Environment.getExternalStoragePublicDirectory; + +/** + * Implementation of this project was made possible via re-use, adoption and improvement of + * following tutorials and resources: + * - http://file.allitebooks.com/20170511/Android%20Sensor%20Programming%20By%20Example.pdf + * - google's simple-pedometer github work licensed under Apache License + * https://github.com/google/simple-pedometer (I initially found it under + * http://gadgetsaint.com/android/create-pedometer-step-counter-android/ who seems to have + * copied it without any reference to original source/work by google) + * - https://notes.iopush.net/android-send-a-https-post-request/ + * - additional help and code has been utilized from + * https://fabcirablog.weebly.com/blog/creating-a-never-ending-background-service-in-android + * to help with services, but also relying on official Android documentation + */ + +public class MainActivity extends AppCompatActivity{ + public static SensorManager sensorManager; + private TextView stepDisplay; + + //tracks a reference to an instance of this class + public static SensorEventListener mainActivitySensorList; + + public static final String TAG = "Actifit"; + + //tracks if listener is active + public static boolean isListenerActive = false; + + private StepsDBHelper mStepsDBHelper; + + //to utilize built-in step sensors + private Sensor stepSensor; + + public static boolean isStepSensorPresent = false; + public static String ACCEL_SENSOR = "ACCEL_SENSOR"; + public static String STEP_SENSOR = "STEP_SENSOR"; + + //enforcing active sensor by default as ACC + public static String activeSensor = MainActivity.ACCEL_SENSOR; + + /* items related to batch data capturing */ + + private int curStepCount = 0; + private static final String BUNDLE_LISTENER = "listener"; + + private static Intent mServiceIntent; + private static ActivityMonitorService mSensorService; + private Context ctx; + + private BroadcastReceiver receiver; + + //flag if service is bound now + boolean mBound = false; + + public Context getCtx() { + return ctx; + } + + static final int REQUEST_TAKE_PHOTO = 1; + + //required function to ask for proper read/write permissions on later Android versions + protected boolean shouldAskPermissions() { + return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1); + } + + public static ActivityMonitorService getmSensorService() { + return mSensorService; + } + + public static void setmSensorService(ActivityMonitorService sensorService) { + mSensorService = sensorService; + } + + public static Intent getmServiceIntent(){ + return mServiceIntent; + } + + public static void setmServiceIntent(Intent serviceIntent){ + mServiceIntent = serviceIntent; + } + + /** + * function checks if the sensor service is running or not + * @param serviceClass + * @return + */ + private boolean isMyServiceRunning(Class serviceClass) { + ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + Log.d(TAG,">>>>[Actifit]isMyServiceRunning?" + true+""); + return true; + } + } + Log.d(TAG,">>>>[Actifit]isMyServiceRunning?" + false+""); + return false; + } + + String mCurrentPhotoPath; + //handles creating the snapped image file + private File createImageFile() throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + //File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File storageDir = getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + File image = File.createTempFile( + imageFileName, /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ + ); + + // Save a file: path for use with ACTION_VIEW intents + mCurrentPhotoPath = image.getAbsolutePath(); + return image; + } + + //security function to detect emulators + public static boolean isEmulator() { + return Build.FINGERPRINT.contains("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") + && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT) + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.HARDWARE.contains("andy"); + } + + //function handles killing the app + private void killActifit(String reason) { + + //display notification to user + Toast toast = Toast.makeText(getApplicationContext(), reason, + Toast.LENGTH_LONG); + + View view = toast.getView(); + + TextView text = view.findViewById(android.R.id.message); + + try { + //Gets the actual oval background of the Toast then sets the colour filter + view.getBackground().setColorFilter(getResources().getColor(R.color.actifitRed), PorterDuff.Mode.SRC_IN); + }catch(Exception e){ + e.printStackTrace(); + } + + text.setTextColor(Color.WHITE); + + toast.show(); + + //kill gracefully + finish(); + } + + + @TargetApi(23) + protected void askPermissions(String[] permissions) { + int requestCode = 200; + requestPermissions(permissions, requestCode); + } + + //function handles checking if the SIM card is available + public boolean isSimAvailable(){ + //standard case covering most phones + TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager.getSimState() != TelephonyManager.SIM_STATE_ABSENT){ + return true; + } + //if we could not identify proper SIM (mostly due to multi-SIM), send an alert to user to fix his status + return false; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + ctx = this; + + //retrieving account data for simple reuse. Data is not stored anywhere outside actifit App. + final SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + /*************** security features ********************/ + + //let's make sure this is a smart phone device by checking SIM Card + + if (!isSimAvailable()){ + //no valid active sim card detected + Log.d(TAG,">>>>[Actifit] No valid SIM card detected"); + killActifit(getString(R.string.no_valid_sim)); + } + + //also let's try to detect if this is a known emulator + if (isEmulator()){ + Log.d(TAG,">>>>[Actifit] Emulator detected"); + killActifit(getString(R.string.emulator_device)); + } + + //check if device is rooted + RootBeer rootBeer = new RootBeer(this); + if(rootBeer.isRootedWithoutBusyBoxCheck()){ + Log.d(TAG,">>>>[Actifit] Device is rooted"); + killActifit(getString(R.string.device_rooted)); + } + + //check if user has a proper unique ID already, if not generate one + String actifitUserID = sharedPreferences.getString("actifitUserID",""); + if (actifitUserID.equals("")) { + actifitUserID = UUID.randomUUID().toString(); + try{ + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String version = pInfo.versionName; + actifitUserID += version; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("actifitUserID", actifitUserID); + editor.apply(); + } + + //Log.d(TAG,"actifitUserID:"+actifitUserID); + + + + //initiate the monitoring service + mSensorService = new ActivityMonitorService(getCtx()); + mServiceIntent = new Intent(getCtx(), mSensorService.getClass()); +//only start the tracking service if the device sensors is picked as tracking medium + String dataTrackingSystem = sharedPreferences.getString("dataTrackingSystem",getString(R.string.device_tracking)); + if (dataTrackingSystem.equals(getString(R.string.device_tracking))) { + + if (!isMyServiceRunning(mSensorService.getClass())) { + startService(mServiceIntent); + } + } + + //grab pointers to specific elements/buttons to be able to capture events and take action + stepDisplay = findViewById(R.id.step_display); + Button BtnViewHistory = findViewById(R.id.btn_view_history); + Button BtnPostSteemit = findViewById(R.id.btn_post_steemit); + Button BtnLeaderboard = findViewById(R.id.btn_view_leaderboard); + Button BtnWallet = findViewById(R.id.btn_view_wallet); + Button BtnSettings = findViewById(R.id.btn_settings); + + Button BtnSnapActiPic = findViewById(R.id.btn_snap_picture); + + Log.d(TAG,">>>>[Actifit] Getting jiggy with it"); + + mStepsDBHelper = new StepsDBHelper(this); + + + //display current date + displayDate(); + + displayUserAndRank(); + + //only display activity count from device if device mode is on + if (dataTrackingSystem.equals(getString(R.string.device_tracking))) { + //set initial steps display value + int stepCount = mStepsDBHelper.fetchTodayStepCount(); + + //display step count while ensuring we don't display negative value if no steps tracked yet + stepDisplay.setText(getString(R.string.activity_today_string) + (stepCount < 0 ? 0 : stepCount)); + }else{ + //inform user that fitbit mode is on + stepDisplay.setText(getString(R.string.fitbit_tracking_mode_active)); + } + + //connecting the activity to the service to receive proper updates on move count + receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int stepCount = intent.getIntExtra("move_count", 0); + stepDisplay.setText(getString(R.string.activity_today_string) + (stepCount < 0 ? 0 : stepCount)); + } + }; + + //handle taking photos + BtnSnapActiPic.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + + //make sure we have a cam on device + PackageManager pm = ctx.getPackageManager(); + + //if no cam, notify and leave + if (!pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)) { + Toast.makeText(getApplicationContext(),"Your device does not have a camera", Toast.LENGTH_SHORT).show(); + return; + } + + //ensure we have proper permissions for image upload + if (shouldAskPermissions()) { + String[] permissions = { + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + }; + askPermissions(permissions); + } + + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + // Ensure that there's a camera activity to handle the intent + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + // Create the File where the photo should go + File photoFile = null; + try { + photoFile = createImageFile(); + } catch (IOException ex) { + // Error occurred while creating the File + ex.printStackTrace(); + } + // Continue only if the File was successfully created + if (photoFile != null) { + + Uri photoURI = FileProvider.getUriForFile(ctx, + "io.actifit.fileprovider", + photoFile); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); + startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); + } + } + } + } + ); + + //handle activity to move to step history screen + BtnViewHistory.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + + Intent intent = new Intent(MainActivity.this, StepHistoryActivity.class); + MainActivity.this.startActivity(intent); + + } + }); + + //handle activity to move to post to steemit screen + BtnPostSteemit.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + + Intent intent = new Intent(MainActivity.this, PostSteemitActivity.class); + MainActivity.this.startActivity(intent); + + } + }); + + //handle activity to move over to the Leaderboard screen + BtnLeaderboard.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + + Intent intent = new Intent(MainActivity.this, LeaderboardActivity.class); + MainActivity.this.startActivity(intent); + + } + }); + + //handle activity to move over to the Wallet screen + BtnWallet.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + + Intent intent = new Intent(MainActivity.this, WalletActivity.class); + MainActivity.this.startActivity(intent); + + } + }); + + //handle activity to move over to the Settings screen + BtnSettings.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View arg0) { + + //sensorManager.unregisterListener(MainActivity.this); + Intent intent = new Intent(MainActivity.this, SettingsActivity.class); + MainActivity.this.startActivity(intent); + + } + }); + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { + galleryAddPic(); + } + } + + //handle appending created pic to the gallery + private void galleryAddPic() { + Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + File f = new File(mCurrentPhotoPath); + Uri contentUri = Uri.fromFile(f); + mediaScanIntent.setData(contentUri); + this.sendBroadcast(mediaScanIntent); + } + + //handles display of local date on front end + private void displayDate(){ + String date_n = new SimpleDateFormat("EEE, MMM dd, yyyy", Locale.getDefault()).format(new Date()); + TextView date = (TextView) findViewById(R.id.current_date); + date.setText(date_n); + } + + //handles fetching and displaying current user and rank + private void displayUserAndRank(){ + //grab stored value, if any + final SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + final String username = sharedPreferences.getString("actifitUser",""); + if (username != "") { + //greet user if user identified + final TextView welcomeUser = findViewById(R.id.welcome_user); + final TextView userRankTV = findViewById(R.id.user_rank); + + //grab user rank if it is already stored today + String userRank = sharedPreferences.getString("userRank", ""); + String userRankUpdateDate = + sharedPreferences.getString("userRankUpdateDate", ""); + Boolean fetchNewRankVal = false; + if (userRank.equals("") || userRankUpdateDate.equals("")){ + fetchNewRankVal = true; + }else{ + //make sure last value is at least within same day, otherwise grab new val + Date date = new Date(); + DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd"); + String strDate = dateFormat.format(date); + if (Integer.parseInt(userRankUpdateDate)() { + @Override + public void onResponse(JSONObject response) { + + // Display the result + try { + //grab current user rank + String userRank = response.getString("user_rank"); + + //store user rank along with date updated + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("userRank", userRank); + + Date date = new Date(); + DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd"); + String strDate = dateFormat.format(date); + + editor.putString("userRankUpdateDate", strDate); + editor.commit(); + + //welcomeUser.setText(getString(R.string.welcome_user).replace("USER_NAME", username).replace("USER_RANK", "(" + userRank + ")")); + userRankTV.setText(userRank+"/100"); + } catch (JSONException e) { + //hide dialog + e.printStackTrace(); + } + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + //hide dialog + error.printStackTrace(); + } + }); + + // Add balance request to be processed + queue.add(balanceRequest); + } + + } + } + + @Override + protected void onStart() { + super.onStart(); + LocalBroadcastManager.getInstance(this).registerReceiver((receiver), + new IntentFilter("ACTIFIT_SERVICE") + ); + + } + + @Override + protected void onResume() { + super.onResume(); + displayDate(); + displayUserAndRank(); + + //ensure our tracking is active particularly after leaving settings + final SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + //only start the tracking service if the device sensors is picked as tracking medium + String dataTrackingSystem = sharedPreferences.getString("dataTrackingSystem",getString(R.string.device_tracking)); + if (dataTrackingSystem.equals(getString(R.string.device_tracking))) { + + if (!isMyServiceRunning(mSensorService.getClass())) { + //initiate the monitoring service + startService(mServiceIntent); + + //enable aggressive mode if set + String aggModeEnabled = sharedPreferences.getString("aggressiveBackgroundTracking",getString(R.string.aggr_back_tracking_off)); + if (aggModeEnabled.equals(getString(R.string.aggr_back_tracking_on))) { + //enable wake lock to ensure tracking functions in the background + PowerManager.WakeLock wl = ActivityMonitorService.getWakeLockInstance(); + if (wl==null){ + //initialize power manager and wake locks either way + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ACTIFIT:ACTIFIT_SPECIAL_LOCK"); + } + if (!wl.isHeld()) { + Log.d(MainActivity.TAG, ">>>>[Actifit]Settings AGG MODE ON"); + wl.acquire(); + } + } + } + }else{ + stepDisplay = findViewById(R.id.step_display); + //inform user that fitbit mode is on + stepDisplay.setText(getString(R.string.fitbit_tracking_mode_active)); + } + + + //LocalBroadcastManager.getInstance(this).registerReceiver((receiver), + // new IntentFilter("ACTIFIT_SERVICE") + //); + } + + @Override + protected void onStop() { + //LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); + super.onStop(); + } + + /* preventing accidental single back button click leading to exiting the app and losing counter tracking */ + boolean doubleBackToExitPressedOnce = false; + @Override + public void onBackPressed() { + if (doubleBackToExitPressedOnce) { + super.onBackPressed(); + return; + } + + this.doubleBackToExitPressedOnce = true; + Toast.makeText(this, getString(R.string.back_exit_confirmation), Toast.LENGTH_SHORT).show(); + + new Handler().postDelayed(new Runnable() { + + @Override + public void run() { + doubleBackToExitPressedOnce = false; + } + }, 2000); + } + + @Override + protected void onDestroy(){ + //sensorManager.unregisterListener(this); + isListenerActive = false; + try{ + if (mServiceIntent!=null) { + stopService(mServiceIntent); + } + }catch(Exception e){ + e.printStackTrace(); + } + super.onDestroy(); + Log.d(TAG,">>>> Actifit destroy state"); + } + + +} \ No newline at end of file diff --git a/MonitorRestartBroadcastReceiver.java b/MonitorRestartBroadcastReceiver.java new file mode 100644 index 00000000..128e1fb7 --- /dev/null +++ b/MonitorRestartBroadcastReceiver.java @@ -0,0 +1,17 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * class handles the re-initiation of the activity sensor monitor in case it was destroyed + */ +public class MonitorRestartBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + /*System.out.println(MonitorRestartBroadcastReceiver.class.getSimpleName() + " Service destroyed!"); + context.startService(new Intent(context, ActivityMonitorService.class));;*/ + } +} diff --git a/MultiSelectionSpinner.java b/MultiSelectionSpinner.java new file mode 100644 index 00000000..eafa5d08 --- /dev/null +++ b/MultiSelectionSpinner.java @@ -0,0 +1,192 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnMultiChoiceClickListener; +import android.util.AttributeSet; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Aneh Thakur on 5/7/2015. + * original source: https://trinitytuts.com/tips/multiselect-spinner-item-in-android/ + */ +public class MultiSelectionSpinner extends Spinner implements + OnMultiChoiceClickListener { + String[] _items = null; + boolean[] mSelection = null; + + ArrayAdapter simple_adapter; + + public MultiSelectionSpinner(Context context) { + super(context); + + simple_adapter = new ArrayAdapter(context, + android.R.layout.simple_spinner_item); + super.setAdapter(simple_adapter); + } + + public MultiSelectionSpinner(Context context, AttributeSet attrs) { + super(context, attrs); + + simple_adapter = new ArrayAdapter(context, + android.R.layout.simple_spinner_item); + super.setAdapter(simple_adapter); + } + + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (mSelection != null && which < mSelection.length) { + mSelection[which] = isChecked; + + simple_adapter.clear(); + simple_adapter.add(buildSelectedItemString()); + } else { + throw new IllegalArgumentException( + "Argument 'which' is out of bounds."); + } + } + + @Override + public boolean performClick() { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setMultiChoiceItems(_items, mSelection, this); + builder.show(); + return true; + } + + @Override + public void setAdapter(SpinnerAdapter adapter) { + throw new RuntimeException( + "setAdapter is not supported by MultiSelectSpinner."); + } + + public void setItems(String[] items) { + _items = items; + mSelection = new boolean[_items.length]; + simple_adapter.clear(); + simple_adapter.add(_items[0]); + Arrays.fill(mSelection, false); + } + + public void setItems(List items) { + _items = items.toArray(new String[items.size()]); + mSelection = new boolean[_items.length]; + simple_adapter.clear(); + simple_adapter.add(_items[0]); + Arrays.fill(mSelection, false); + } + + public void setSelection(String[] selection) { + for (String cell : selection) { + for (int j = 0; j < _items.length; ++j) { + if (_items[j].equals(cell)) { + mSelection[j] = true; + } + } + } + } + + public void setSelection(List selection) { + for (int i = 0; i < mSelection.length; i++) { + mSelection[i] = false; + } + for (String sel : selection) { + for (int j = 0; j < _items.length; ++j) { + if (_items[j].equals(sel)) { + mSelection[j] = true; + } + } + } + simple_adapter.clear(); + simple_adapter.add(buildSelectedItemString()); + } + + public void setSelection(int index) { + for (int i = 0; i < mSelection.length; i++) { + mSelection[i] = false; + } + if (index >= 0 && index < mSelection.length) { + mSelection[index] = true; + } else { + throw new IllegalArgumentException("Index " + index + + " is out of bounds."); + } + simple_adapter.clear(); + simple_adapter.add(buildSelectedItemString()); + } + + public void setSelection(int[] selectedIndicies) { + for (int i = 0; i < mSelection.length; i++) { + mSelection[i] = false; + } + for (int index : selectedIndicies) { + if (index >= 0 && index < mSelection.length) { + mSelection[index] = true; + } else { + throw new IllegalArgumentException("Index " + index + + " is out of bounds."); + } + } + simple_adapter.clear(); + simple_adapter.add(buildSelectedItemString()); + } + + public List getSelectedStrings() { + List selection = new LinkedList(); + for (int i = 0; i < _items.length; ++i) { + if (mSelection[i]) { + selection.add(_items[i]); + } + } + return selection; + } + + public List getSelectedIndicies() { + List selection = new LinkedList(); + for (int i = 0; i < _items.length; ++i) { + if (mSelection[i]) { + selection.add(i); + } + } + return selection; + } + + private String buildSelectedItemString() { + StringBuilder sb = new StringBuilder(); + boolean foundOne = false; + + for (int i = 0; i < _items.length; ++i) { + if (mSelection[i]) { + if (foundOne) { + sb.append(", "); + } + foundOne = true; + + sb.append(_items[i]); + } + } + return sb.toString(); + } + + public String getSelectedItemsAsString() { + StringBuilder sb = new StringBuilder(); + boolean foundOne = false; + + for (int i = 0; i < _items.length; ++i) { + if (mSelection[i]) { + if (foundOne) { + sb.append(", "); + } + foundOne = true; + sb.append(_items[i]); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/NxFitbitHelper.java b/NxFitbitHelper.java new file mode 100644 index 00000000..28ff5a58 --- /dev/null +++ b/NxFitbitHelper.java @@ -0,0 +1,189 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.net.Uri; +import android.os.StrictMode; +import android.support.customtabs.CustomTabsIntent; +import android.util.Log; + +import com.github.scribejava.apis.FitbitApi20; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import java.util.concurrent.ExecutionException; + + +/** + * this class and relevant Fitbit work across our Actifit solution has been constructed and adapted + * based on great work done at https://git.nxfifteen.rocks/nxad/test-android-fitbit-oauth/ + * as well as Fitbit official documentation + */ + + +class NxFitbitHelper { + @SuppressWarnings("FieldCanBeLocal") + private String clientid = ""; + @SuppressWarnings("FieldCanBeLocal") + private String clientSecret = ""; + @SuppressWarnings("FieldCanBeLocal") + private String apiCallback = ""; + @SuppressWarnings("FieldCanBeLocal") + private String apiScope = "activity heartrate location profile weight"; + + private JSONObject apiValueProfile; + + private String authCode = ""; + private OAuth20Service service; + private OAuth2AccessToken accessToken; + + private String fitBitAPIUrl = "https://api.fitbit.com/1/"; + + //constructor + NxFitbitHelper(Context ctxt) { + clientid = ctxt.getString(R.string.fitbit_clientid); + clientSecret = ctxt.getString(R.string.fitbit_clientsec); + apiCallback = ctxt.getString(R.string.fitbit_apicb); + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + } + + //function handles redirecting user to proper authorization url via customtabs + static void sendUserToAuthorisation(Context callingContext) { + + NxFitbitHelper helperClass = new NxFitbitHelper(callingContext); + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + + builder.setToolbarColor(callingContext.getResources().getColor(R.color.actifitRed)); + + //animation for showing and closing fitbit authorization screen + builder.setStartAnimations(callingContext, R.anim.slide_in_right, R.anim.slide_out_left); + + //animation for back button clicks + builder.setExitAnimations(callingContext, android.R.anim.slide_in_left, + android.R.anim.slide_out_right); + + CustomTabsIntent customTabsIntent = builder.build(); + + customTabsIntent.launchUrl(callingContext, Uri.parse(helperClass.getAuthorizationUrl())); + } + + private String getAuthorizationUrl() { + return getService().getAuthorizationUrl(); + } + + //function handles building up connection to fitbit API with relevant params + private OAuth20Service createService() { + return new ServiceBuilder(clientid) + .apiSecret(clientSecret) + .scope(apiScope) + .callback(apiCallback) + .build(FitbitApi20.instance()); + } + + private OAuth20Service getService() { + if (service == null) { + service = createService(); + } + return service; + } + + private void setAuthCodeFromIntent(Uri returnUrl) { + authCode = returnUrl.getQueryParameter("code"); + Log.d(MainActivity.TAG,"Auth Code set to " + authCode); + } + + //function handles connecting and grabbing an access token + private void requestAccessToken() { + try { + OAuth20Service serviceCall = getService(); + accessToken = serviceCall.getAccessToken(authCode); + Log.d(MainActivity.TAG,"accesstoken=" + accessToken); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + //function handles processing actual calls to grab Fitbit data for syncing purposes + JSONObject makeApiRequest(String endPointUrl) throws InterruptedException, ExecutionException, IOException { + OAuth20Service serviceCall = getService(); + + final OAuthRequest activityrequest = new OAuthRequest(Verb.GET, fitBitAPIUrl + endPointUrl); + serviceCall.signRequest(accessToken, activityrequest); + + final Response response; + response = serviceCall.execute(activityrequest); + + try { + return new JSONObject(response.getBody()); + } catch (JSONException e) { + return null; + } + } + + void requestAccessTokenFromIntent(Uri returnUrl) { + setAuthCodeFromIntent(returnUrl); + requestAccessToken(); + } + + JSONObject getUserProfile() throws InterruptedException, ExecutionException, IOException { + if (apiValueProfile == null) { + apiValueProfile = makeApiRequest("user/-/profile.json"); + } + return apiValueProfile; + } + + // Function handles retrieving all auto-recorded recorded today for this user + // SoughtInfo param decides whether we need: "step", "distance", or other available options + // For more info https://dev.fitbit.com/build/reference/web-api/activity/ + JSONObject getTodayActivity(String soughtInfo) throws InterruptedException, ExecutionException, IOException { + + // Define which activities we are interested in. + // Here we only look for auto-registered activities which is fetched using this param + // For more info https://dev.fitbit.com/build/reference/web-api/activity/ + // Format used: //user/[user-id]/[resource-path]/date/[date]/[period].json + String queryFormat = "user/-/activities/tracker/" + soughtInfo + "/date/today/1d.json"; + + return makeApiRequest(queryFormat ); + + } + + String getFieldFromProfile(String jsonFieldName) { + String jsonFieldValue = "An error occurred"; + if (apiValueProfile == null) { + try { + apiValueProfile = makeApiRequest("user/-/profile.json"); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e){ + e.printStackTrace(); + } + } + + try { + jsonFieldValue = apiValueProfile.getJSONObject("user").getString(jsonFieldName); + } catch (JSONException e) { + e.printStackTrace(); + } + + return jsonFieldValue; + } + +} diff --git a/PostSteemitActivity.java b/PostSteemitActivity.java new file mode 100644 index 00000000..cb444026 --- /dev/null +++ b/PostSteemitActivity.java @@ -0,0 +1,918 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.os.StrictMode; + +import android.provider.MediaStore; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.support.v7.widget.Toolbar; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.amazonaws.mobileconnectors.s3.transferutility.*; + +import com.amazonaws.mobile.client.AWSMobileClient; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.util.IOUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import com.mittsu.markedview.MarkedView; + + +public class PostSteemitActivity extends AppCompatActivity implements View.OnClickListener{ + + private StepsDBHelper mStepsDBHelper; + private String notification = ""; + private int min_step_limit = 1000; + private int min_char_count = 100; + private Context steemit_post_context; + + //track Choosing Image Intent + private static final int CHOOSING_IMAGE_REQUEST = 1234; + + private EditText steemitPostContent; + + private Uri fileUri; + private Bitmap bitmap; + private ImageView image_preview; + + private EditText stepCountContainer; + private Activity currentActivity; + + //tracks whether user synched his Fitbit data to avoid refetching activity count from current device + private int fitbitSyncDone = 0; + + //required function to ask for proper read/write permissions on later Android versions + protected boolean shouldAskPermissions() { + return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1); + } + + @TargetApi(23) + protected void askPermissions() { + String[] permissions = { + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE" + }; + int requestCode = 200; + requestPermissions(permissions, requestCode); + } + + + //implementing file upload functionality + private void uploadFile() { + final ProgressDialog uploadProgress; + if (fileUri != null) { + + //create unique image file name + final String fileName = UUID.randomUUID().toString(); + + final File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + "/" + fileName); + + createFile(getApplicationContext(), fileUri, file); + + TransferUtility transferUtility = + TransferUtility.builder() + .context(getApplicationContext()) + .awsConfiguration(AWSMobileClient.getInstance().getConfiguration()) + .s3Client(new AmazonS3Client(AWSMobileClient.getInstance().getCredentialsProvider())) + .build(); + + //specify content type to be image to be properly recognizable upon rendering + ObjectMetadata imgMetaData = new ObjectMetadata(); + imgMetaData.setContentType("image/jpeg"); + + TransferObserver uploadObserver = + transferUtility.upload(fileName, file, imgMetaData); + + //create a new progress dialog to show action is underway + uploadProgress = new ProgressDialog(steemit_post_context); + uploadProgress.setMessage("Uploading...0%"); + uploadProgress.show(); + + uploadObserver.setTransferListener(new TransferListener() { + + @Override + public void onStateChanged(int id, TransferState state) { + if (TransferState.COMPLETED == state) { + try { + if (uploadProgress != null && uploadProgress.isShowing()) { + uploadProgress.dismiss(); + } + }catch (Exception ex){ + Log.d(MainActivity.TAG,ex.getMessage()); + } + + Toast.makeText(getApplicationContext(), "Upload Completed!", Toast.LENGTH_SHORT).show(); + + String full_img_url = getString(R.string.actifit_usermedia_url)+fileName; + String img_markdown_text = "![]("+full_img_url+")"; + + //append the uploaded image url to the text as markdown + //if there is any particular selection, replace it too + + int start = Math.max(steemitPostContent.getSelectionStart(), 0); + int end = Math.max(steemitPostContent.getSelectionEnd(), 0); + steemitPostContent.getText().replace(Math.min(start, end), Math.max(start, end), + img_markdown_text, 0, img_markdown_text.length()); + + file.delete(); + + } else if (TransferState.FAILED == state) { + Toast toast = Toast.makeText(getApplicationContext(), "Upload Failed!", Toast.LENGTH_SHORT); + TextView v = toast.getView().findViewById(android.R.id.message); + v.setTextColor(Color.RED); + toast.show(); + file.delete(); + } + } + + @Override + public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) { + float percentDonef = ((float) bytesCurrent / (float) bytesTotal) * 100; + int percentDone = (int) percentDonef; + uploadProgress.setMessage("Uploading..."+percentDone + "%"); + //tvFileName.setText("ID:" + id + "|bytesCurrent: " + bytesCurrent + "|bytesTotal: " + bytesTotal + "|" + percentDone + "%"); + } + + @Override + public void onError(int id, Exception ex) { + ex.printStackTrace(); + } + + }); + } + } + + @Override + public void onClick(View view) { + int i = view.getId(); + + if (i == R.id.btn_choose_file) { + showChoosingFile(); + } /*else if (i == R.id.btn_upload) { + uploadFile(); + }*/ + } + + //handles the display of image selection + private void showChoosingFile() { + + //ensure we have proper permissions for image upload + if (shouldAskPermissions()) { + askPermissions(); + } + + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, "Select Image"), CHOOSING_IMAGE_REQUEST); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (bitmap != null) { + bitmap.recycle(); + } + + if (requestCode == CHOOSING_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) { + fileUri = data.getData(); + try { + bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), fileUri); + + uploadFile(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + //handle displaying a preview of selected image + private void setPic(String mCurrentPhotoPath) { + // Get the dimensions of the View + int targetW = image_preview.getWidth(); + int targetH = image_preview.getHeight(); + + // Get the dimensions of the bitmap + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + bmOptions.inJustDecodeBounds = true; + BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions); + int photoW = bmOptions.outWidth; + int photoH = bmOptions.outHeight; + + // Determine how much to scale down the image + int scaleFactor = Math.min(photoW/targetW, photoH/targetH); + + // Decode the image file into a Bitmap sized to fill the View + bmOptions.inJustDecodeBounds = false; + bmOptions.inSampleSize = scaleFactor; + bmOptions.inPurgeable = true; + + Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions); + image_preview.setImageBitmap(bitmap); + } + + private void createFile(Context context, Uri srcUri, File dstFile) { + try { + InputStream inputStream = context.getContentResolver().openInputStream(srcUri); + if (inputStream == null) return; + OutputStream outputStream = new FileOutputStream(dstFile); + IOUtils.copy(inputStream, outputStream); + inputStream.close(); + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_post_steemit); + + + Toolbar postToolbar = findViewById(R.id.post_toolbar); + setSupportActionBar(postToolbar); + + + //make sure help with PPKey link click works + TextView ppHelpLink = findViewById(R.id.posting_key_link); + ppHelpLink.setMovementMethod(LinkMovementMethod.getInstance()); + + //setting context + this.steemit_post_context = this; + + //getting an instance of DB handler + mStepsDBHelper = new StepsDBHelper(this); + + //grabbing instances of input data sources + stepCountContainer = findViewById(R.id.steemit_step_count); + + //set initial steps display value + int stepCount = mStepsDBHelper.fetchTodayStepCount(); + //display step count while ensuring we don't display negative value if no steps tracked yet + stepCountContainer.setText(String.valueOf((stepCount<0?0:stepCount)), TextView.BufferType.EDITABLE); + + + EditText steemitPostTitle = findViewById(R.id.steemit_post_title); + EditText steemitUsername = findViewById(R.id.steemit_username); + EditText steemitPostingKey = findViewById(R.id.steemit_posting_key); + steemitPostContent = findViewById(R.id.steemit_post_text); + TextView measureSectionLabel = findViewById(R.id.measurements_section_lbl); + + TextView heightSizeUnit = findViewById(R.id.measurements_height_unit); + TextView weightSizeUnit = findViewById(R.id.measurements_weight_unit); + TextView waistSizeUnit = findViewById(R.id.measurements_waistsize_unit); + TextView chestSizeUnit = findViewById(R.id.measurements_chest_unit); + TextView thighsSizeUnit = findViewById(R.id.measurements_thighs_unit); + + final MarkedView mdView = (MarkedView)findViewById(R.id.md_view); + // call from code + // MarkedView mdView = new MarkedView(this); + + // set markdown text pattern. ('contents' object is markdown text) + mdView.setMDText(steemitPostContent.getText().toString()); + + //retrieving account data for simple reuse. Data is not stored anywhere outside actifit App. + final SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + //try to load editor content if it was stored previously + steemitPostContent.setText(sharedPreferences.getString("steemPostContent","")); + + //hook change event for report content preview and saving the text to prevent data loss + steemitPostContent.addTextChangedListener(new TextWatcher() { + + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + if(s.length() != 0) { + mdView.setMDText(steemitPostContent.getText().toString()); + + //store current text + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString("steemPostContent", + steemitPostContent.getText().toString()); + editor.apply(); + } + } + }); + + //image_preview = findViewById(R.id.image_preview); + + //initialize AWS settings and configuration + + findViewById(R.id.btn_choose_file).setOnClickListener(this); + + AWSMobileClient.getInstance().initialize(this).execute(); + + //Adding default title content for the daily post + + //generating today's date + Calendar mCalendar = Calendar.getInstance(); + String postTitle = getString(R.string.default_post_title); + postTitle += " "+new SimpleDateFormat("MMMM d yyyy").format(mCalendar.getTime()); + + //postTitle += String.valueOf(mCalendar.get(Calendar.MONTH)+1)+" " + + //String.valueOf(mCalendar.get(Calendar.DAY_OF_MONTH))+"/"+String.valueOf(mCalendar.get(Calendar.YEAR)); + steemitPostTitle.setText(postTitle); + + //initializing activity options + String[] activity_type = { + "Walking", "Jogging", "Running", "Cycling", "Rope Skipping", + "Dancing","Basketball", "Football", "Boxing", "Tennis", "Table Tennis", + "Martial Arts", "House Chores", "Moving Around Office", "Shopping","Daily Activity", + "Aerobics", "Weight Lifting", "Treadmill","Stair Mill", "Elliptical", + "Hiking", "Gardening", "Rollerblading", "Cricket", "Golf", "Volleyball", "Geocaching", + "Shoveling" + }; + + //sort options in alpha order + Arrays.sort(activity_type); + + MultiSelectionSpinner activityTypeSelector = findViewById(R.id.steemit_activity_type); + activityTypeSelector.setItems(activity_type); + + steemitUsername.setText(sharedPreferences.getString("actifitUser","")); + steemitPostingKey.setText(sharedPreferences.getString("actifitPst","")); + + //grab current selection for measure system + String activeSystem = sharedPreferences.getString("activeSystem",getString(R.string.metric_system)); + //adjust units accordingly + if (activeSystem.equals(getString(R.string.metric_system))){ + weightSizeUnit.setText("kg"); + heightSizeUnit.setText("cm"); + waistSizeUnit.setText("cm"); + chestSizeUnit.setText("cm"); + thighsSizeUnit.setText("cm"); + }else{ + weightSizeUnit.setText("lb"); + heightSizeUnit.setText("ft"); + waistSizeUnit.setText("in"); + chestSizeUnit.setText("in"); + thighsSizeUnit.setText("in"); + } + + currentActivity = this; + + //capturing steemit post submission + Button BtnSubmitSteemit = findViewById(R.id.post_to_steem_btn); + BtnSubmitSteemit.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View arg0) { + ProcessPost(); + } + + }); + + /* fixing scrollability of content within the post content section */ + + steemitPostContent.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (v.getId() == R.id.steemit_post_text) { + + v.getParent().requestDisallowInterceptTouchEvent(true); + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_UP: + v.getParent().requestDisallowInterceptTouchEvent(false); + break; + } + + } + return false; + } + }); + + + /***************** Fitbit Sync Implementation ****************/ + + //capturing fitbit sync action + Button BtnFitbitSync = findViewById(R.id.fitbit_sync); + BtnFitbitSync.setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(final View arg0) { + // Connect to fitbit and grab data + NxFitbitHelper.sendUserToAuthorisation(steemit_post_context); + } + + }); + + //retrieve resulting data from fitbit sync (parameter from the Intent) + Uri returnUrl = getIntent().getData(); + if (returnUrl != null) { + NxFitbitHelper fitbit = new NxFitbitHelper(getApplicationContext()); + fitbit.requestAccessTokenFromIntent(returnUrl); + + // Get user profile using helper function + try { + JSONObject responseProfile = fitbit.getUserProfile(); + Log.d(MainActivity.TAG, "From JSON encodedId: " + responseProfile.getJSONObject("user").getString("encodedId")); + Log.d(MainActivity.TAG, "From JSON fullName: " + responseProfile.getJSONObject("user").getString("fullName")); + + //check to see if settings allows fetching measurements - default true + String fetchMeasurements = sharedPreferences.getString("fitbitMeasurements",getString(R.string.fitbit_measurements_on)); + if (fetchMeasurements.equals(getString(R.string.fitbit_measurements_on))) { + //grab and update user weight + TextView weight = findViewById(R.id.measurements_weight); + weight.setText(fitbit.getFieldFromProfile("weight")); + + //grab and update user height + TextView height = findViewById(R.id.measurements_height); + height.setText(fitbit.getFieldFromProfile("height")); + } + + } catch (JSONException | InterruptedException | ExecutionException | IOException e){ + e.printStackTrace(); + } catch (Exception e){ + e.printStackTrace(); + } + + try { + String soughtInfo = "steps"; + JSONObject stepActivityList = fitbit.getTodayActivity(soughtInfo); + JSONArray stepActivityArray = stepActivityList.getJSONArray("activities-tracker-"+soughtInfo); + Log.d(MainActivity.TAG, "From JSON distance:" + stepActivityArray.length() ); + int trackedActivityCount = 0; + if (stepActivityArray.length()>0){ + Log.d(MainActivity.TAG, "we found matching records"); + //loop through records adding up recorded steps + for (int i=0;i>>Finish"); + currentActivity.finish(); + } + } + }); + //create and display alert window + AlertDialog alert11 = builder1.create(); + alert11.show(); + } + }); + + //finish(); + } + + private class PostSteemitRequest extends AsyncTask { + ProgressDialog progress; + private final Context context; + private Activity currentActivity; + public PostSteemitRequest(Context c, Activity currentActivity){ + this.context = c; + this.currentActivity = currentActivity; + } + protected void onPreExecute(){ + //create a new progress dialog to show action is underway + progress = new ProgressDialog(this.context); + progress.setMessage(getString(R.string.sending_post)); + progress.show(); + } + protected Void doInBackground(String... params) { + try { + Log.d(MainActivity.TAG,"click"); + + //disable button to prevent multiple clicks + //arg0.setEnabled(false); + + EditText steemitPostTitle = findViewById(R.id.steemit_post_title); + EditText steemitUsername = findViewById(R.id.steemit_username); + EditText steemitPostingKey = findViewById(R.id.steemit_posting_key); + EditText steemitPostContent = findViewById(R.id.steemit_post_text); + EditText steemitPostTags = findViewById(R.id.steemit_post_tags); + EditText steemitStepCount = findViewById(R.id.steemit_step_count); + MultiSelectionSpinner activityTypeSelector = findViewById(R.id.steemit_activity_type); + + CheckBox fullAFITPay = findViewById(R.id.full_afit_pay); + + EditText heightSize = findViewById(R.id.measurements_height); + EditText weightSize = findViewById(R.id.measurements_weight); + EditText bodyFat = findViewById(R.id.measurements_bodyfat); + EditText chestSize = findViewById(R.id.measurements_chest); + EditText thighsSize = findViewById(R.id.measurements_thighs); + EditText waistSize = findViewById(R.id.measurements_waistsize); + + TextView heightSizeUnit = findViewById(R.id.measurements_height_unit); + TextView weightSizeUnit = findViewById(R.id.measurements_weight_unit); + TextView waistSizeUnit = findViewById(R.id.measurements_waistsize_unit); + TextView chestSizeUnit = findViewById(R.id.measurements_chest_unit); + TextView thighsSizeUnit = findViewById(R.id.measurements_thighs_unit); + + + //storing account data for simple reuse. Data is not stored anywhere outside actifit App. + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + //skip on spaces, upper case, and @ symbols to properly match steem username patterns + editor.putString("actifitUser", steemitUsername.getText().toString() + .trim().toLowerCase().replace("@","")); + editor.putString("actifitPst", steemitPostingKey.getText().toString()); + editor.apply(); + + //this runs only on live mode + if (getString(R.string.test_mode).equals("off")){ + //make sure we have reached the min movement amount + if (Integer.parseInt(steemitStepCount.getText().toString()) < min_step_limit) { + notification = "You have not reached the minimum " + + NumberFormat.getNumberInstance(Locale.US).format(min_step_limit) + " activity yet"; + displayNotification(notification, progress, context, currentActivity, ""); + + return null; + } + + //make sure the post content has at least the min_char_count + if (steemitPostContent.getText().toString().length() + <= min_char_count){ + notification = getString(R.string.min_char_count_error) + +" "+ min_char_count + +" "+ getString(R.string.characters_plural_label); + displayNotification(notification, progress, context, currentActivity, ""); + + return null; + } + + //make sure the user has not posted today already, + //and also avoid potential abuse of changing phone clock via comparing to older dates + String lastPostDate = sharedPreferences.getString("actifitLastPostDate",""); + String currentDate = new SimpleDateFormat("yyyyMMdd").format( + Calendar.getInstance().getTime()); + + Log.d(MainActivity.TAG,">>>>[Actifit]lastPostDate:"+lastPostDate); + Log.d(MainActivity.TAG,">>>>[Actifit]currentDate:"+currentDate); + if (!lastPostDate.equals("")){ + if (Integer.parseInt(lastPostDate) >= Integer.parseInt(currentDate)) { + notification = getString(R.string.one_post_per_day_error); + displayNotification(notification, progress, context, currentActivity, ""); + return null; + } + } + + } + + //let us check if user has selected activities yet + if (activityTypeSelector.getSelectedIndicies().size()<1){ + notification = getString(R.string.error_need_select_one_activity); + displayNotification(notification, progress, context, currentActivity, ""); + + //reset to enabled + //arg0.setEnabled(true); + return null; + } + + + + + //prepare data to be sent along post + final JSONObject data = new JSONObject(); + try { + //skip on spaces, upper case, and @ symbols to properly match steem username patterns + data.put("author", steemitUsername.getText().toString() + .trim().toLowerCase().replace("@","")); + data.put("posting_key", steemitPostingKey.getText()); + data.put("title", steemitPostTitle.getText()); + data.put("content", steemitPostContent.getText()); + data.put("tags", steemitPostTags.getText()); + data.put("step_count", steemitStepCount.getText()); + data.put("activity_type", activityTypeSelector.getSelectedItemsAsString()); + + if (fullAFITPay.isChecked()) { + data.put("full_afit_pay", "on"); + } + + data.put("height", heightSize.getText()); + data.put("weight", weightSize.getText()); + data.put("chest", chestSize.getText()); + data.put("waist", waistSize.getText()); + data.put("thighs", thighsSize.getText()); + data.put("bodyfat", bodyFat.getText()); + + data.put("heightUnit", heightSizeUnit.getText()); + data.put("weightUnit", weightSizeUnit.getText()); + data.put("chestUnit", chestSizeUnit.getText()); + data.put("waistUnit", waistSizeUnit.getText()); + data.put("thighsUnit", thighsSizeUnit.getText()); + + data.put("appType", "Android"); + + + //appending security param values + data.put( getString(R.string.sec_param), getString(R.string.sec_param_val)); + + //grab app version number + try { + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String version = pInfo.versionName; + data.put("appVersion",version); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + + + //choose a charity if one is already selected before + + sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + final String currentCharity = (sharedPreferences.getString("selectedCharity","")); + + if (!currentCharity.equals("")){ + data.put("charity", currentCharity); + } + + //append user ID + data.put("actifitUserID", sharedPreferences.getString("actifitUserID","")); + + //append data tracking source to see if this is a device reading or a fitbit one + data.put("dataTrackingSource",sharedPreferences.getString("dataTrackingSystem","")); + + //append report STEEM payout type + data.put("reportSTEEMPayMode",sharedPreferences.getString("reportSTEEMPayMode","")); + + } catch (JSONException e) { + e.printStackTrace(); + } + + String inputLine; + String result = ""; + //use test url only if testing mode is on + String urlStr = getString(R.string.test_api_url); + if (getString(R.string.test_mode).equals("off")) { + urlStr = getString(R.string.api_url); + } + // Headers + ArrayList headers = new ArrayList<>(); + + headers.add(new String[]{"Content-Type", "application/json"}); + HttpResultHelper httpResult = new HttpResultHelper(); + + httpResult = httpResult.httpPost(urlStr, null, null, data.toString(), headers, 20000); + BufferedReader in = new BufferedReader(new InputStreamReader(httpResult.getResponse())); + while ((inputLine = in.readLine()) != null) { + result += inputLine; + } + + Log.d(MainActivity.TAG,">>>test:" + result); + + //check result of action + if (result.equals("success")) { + notification = getString(R.string.success_post); + + //store date of last successful post to prevent multiple posts per day + + //storing account data for simple reuse. Data is not stored anywhere outside actifit App. + sharedPreferences = getSharedPreferences("actifitSets", MODE_PRIVATE); + editor = sharedPreferences.edit(); + editor.putString("actifitLastPostDate", + new SimpleDateFormat("yyyyMMdd").format( + Calendar.getInstance().getTime())); + //also clear editor text content + editor.putString("steemPostContent", ""); + editor.apply(); + } else { + // notification = getString(R.string.failed_post); + notification = result; + } + + //display proper notification + displayNotification(notification, progress, context, currentActivity, result); + + }catch (Exception e){ + + //display proper notification + notification = getString(R.string.failed_post); + displayNotification(notification, progress, context, currentActivity, ""); + + Log.d(MainActivity.TAG,"Error connecting:"+e.getMessage()); + e.printStackTrace(); + } + return null; + } + } + + private void ProcessPost(){ + + //only if we haven't grabbed fitbit data, we need to grab new sensor data + if (fitbitSyncDone == 0){ + //ned to grab new updated activity count before posting + int stepCount = mStepsDBHelper.fetchTodayStepCount(); + + //display step count while ensuring we don't display negative value if no steps tracked yet + stepCountContainer.setText(String.valueOf((stepCount<0?0:stepCount)), TextView.BufferType.EDITABLE); + }else{ + //need to check if a day has passed, to prevent posting again using same fitbit data + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + String lastSyncDate = sharedPreferences.getString("fitbitLastSyncDate",""); + String currentDate = new SimpleDateFormat("yyyyMMdd").format( + Calendar.getInstance().getTime()); + + Log.d(MainActivity.TAG,">>>>[Actifit]lastPostDate:"+lastSyncDate); + Log.d(MainActivity.TAG,">>>>[Actifit]currentDate:"+currentDate); + if (!lastSyncDate.equals("")){ + if (Integer.parseInt(lastSyncDate) < Integer.parseInt(currentDate)) { + notification = getString(R.string.need_sync_fitbit_again); + ProgressDialog progress = new ProgressDialog(steemit_post_context); + progress.setMessage(notification); + progress.show(); + displayNotification(notification, progress, steemit_post_context, currentActivity, ""); + return; + } + } + + } + + + //we need to check first if we have a charity setup + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + final String currentCharity = sharedPreferences.getString("selectedCharity",""); + final String currentCharityDisplayName = sharedPreferences.getString("selectedCharityDisplayName",""); + + if (!currentCharity.equals("")){ + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + //go ahead posting + new PostSteemitRequest(steemit_post_context, currentActivity).execute(); + break; + + case DialogInterface.BUTTON_NEGATIVE: + //cancel + break; + } + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(steemit_post_context); + builder.setMessage(getString(R.string.current_workout_going_charity) + " " + + currentCharityDisplayName + " " + + getString(R.string.current_workout_settings_based)) + .setPositiveButton("Yes", dialogClickListener) + .setNegativeButton("No", dialogClickListener).show(); + }else { + //connect to the server via a thread to prevent application hangup + new PostSteemitRequest(steemit_post_context, currentActivity).execute(); + } + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + //getMenuInflater().inflate(R.menu.post_steem_menu, menu); + return true; + } + + + //handle the menu item click (new post to steem button) + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case R.id.action_favorite: + ProcessPost(); + return true; + + default: + // If we got here, the user's action was not recognized. + // Invoke the superclass to handle it. + return super.onOptionsItemSelected(item); + + } + } + +} + diff --git a/ReminderNotificationService.java b/ReminderNotificationService.java new file mode 100644 index 00000000..39236306 --- /dev/null +++ b/ReminderNotificationService.java @@ -0,0 +1,85 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +/** + * Class handles sending out reminder notifications in case user sets them + */ +public class ReminderNotificationService extends BroadcastReceiver { + public static final int NOTIFICATION_ID = 2194143; + + + @Override + public void onReceive(Context context, Intent intent) { + + //let's check first if user had posted already so as to avoid sending a useless reminder + SharedPreferences sharedPreferences = context.getSharedPreferences("actifitSets", context.MODE_PRIVATE); + String lastPostDate = sharedPreferences.getString("actifitLastPostDate",""); + String currentDate = new SimpleDateFormat("yyyyMMdd").format( + Calendar.getInstance().getTime()); + + if (!lastPostDate.equals("")) { + if (Integer.parseInt(lastPostDate) >= Integer.parseInt(currentDate)) { + //do nothing as the user has already posted today + return; + } + } + + //alternate path, let's notify + + //support for Android 8+ + createNotificationChannel(context); + + Intent notifyIntent = new Intent(context, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, 0); + + //prepare notification details + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, + context.getString(R.string.actifit_channel_remind_ID)) + .setContentTitle("Actifit Reminder!") + .setContentText(context.getString(R.string.daily_post_reminder)) + .setSmallIcon(R.drawable.actifit_logo) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .setContentIntent(pendingIntent); + + Notification notificationCompat = builder.build(); + + //proceed notifying user + NotificationManagerCompat managerCompat = NotificationManagerCompat.from(context); + managerCompat.notify(NOTIFICATION_ID, notificationCompat); + } + + + private void createNotificationChannel(Context context) { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = context.getString(R.string.actifit_notif_channel); + String description = context.getString(R.string.actifit_notif_description); + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel( + context.getString(R.string.actifit_channel_remind_ID), name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + +} diff --git a/SensorFusionMath.java b/SensorFusionMath.java new file mode 100644 index 00000000..ed843dfc --- /dev/null +++ b/SensorFusionMath.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013 Google Inc. + * + * 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 io.actifit.fitnesstracker.actifitfitnesstracker; + +public class SensorFusionMath { + + private SensorFusionMath () { + } + + public static float sum(float[] array) { + float retval = 0; + for (int i = 0; i < array.length; i++) { + retval += array[i]; + } + return retval; + } + + public static float[] cross(float[] arrayA, float[] arrayB) { + float[] retArray = new float[3]; + retArray[0] = arrayA[1] * arrayB[2] - arrayA[2] * arrayB[1]; + retArray[1] = arrayA[2] * arrayB[0] - arrayA[0] * arrayB[2]; + retArray[2] = arrayA[0] * arrayB[1] - arrayA[1] * arrayB[0]; + return retArray; + } + + public static float norm(float[] array) { + float retval = 0; + for (int i = 0; i < array.length; i++) { + retval += array[i] * array[i]; + } + return (float) Math.sqrt(retval); + } + + // Note: only works with 3D vectors. + public static float dot(float[] a, float[] b) { + float retval = a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + return retval; + } + + public static float[] normalize(float[] a) { + float[] retval = new float[a.length]; + float norm = norm(a); + for (int i = 0; i < a.length; i++) { + retval[i] = a[i] / norm; + } + return retval; + } +} diff --git a/SettingsActivity.java b/SettingsActivity.java new file mode 100644 index 00000000..fd719245 --- /dev/null +++ b/SettingsActivity.java @@ -0,0 +1,458 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.support.annotation.NonNull; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.NumberPicker; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonArrayRequest; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Random; + +import static io.actifit.fitnesstracker.actifitfitnesstracker.MainActivity.isStepSensorPresent; + +public class SettingsActivity extends AppCompatActivity { + + private NumberPicker hourOptions, minOptions; + private AlarmManager alarmManager; + private PendingIntent alarmIntent; + private String fullSPPay = "full_SP_Pay"; + private String sbdSPPay = "50_50_SBD_SP_Pay"; + + /*@Bind(R.id.main_toolbar) + Toolbar toolbar;*/ + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + + //display version number + try { + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String version = pInfo.versionName; + TextView version_info = findViewById(R.id.version_info); + version_info.setText("Actifit App Version: "+version); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + //grab instances of settings components + final RadioButton metricSysRadioBtn = findViewById(R.id.metric_system); + final RadioButton usSystemRadioBtn = findViewById(R.id.us_system); + + final CheckBox aggBgTrackingChckBox = findViewById(R.id.background_tracking); + + final CheckBox donateCharityChckBox = findViewById(R.id.donate_charity); + + final CheckBox reminderSetChckBox = findViewById(R.id.reminder_settings); + + final RadioButton deviceSensorsBtn = findViewById(R.id.device_sensors); + final RadioButton fitbitBtn = findViewById(R.id.fitbit); + final LinearLayout aggModeSection = findViewById(R.id.background_tracking_section); + + final LinearLayout fitbitSettingsSection = findViewById(R.id.fitbit_settings_section); + final CheckBox fitbitMeasurementsChckBox = findViewById(R.id.fitbit_measurements); + + final RadioButton fullSPayRadioBtn = findViewById(R.id.full_sp_pay); + final RadioButton sbdSPPayRadioBtn = findViewById(R.id.sbd_sp_pay); + + + Spinner charitySelected = findViewById(R.id.charity_options); + + //retrieving prior settings if already saved before + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + String currentSystem = (sharedPreferences.getString("activeSystem","")); + + //check which is the current active system + //if the setting is manually set as US System or default Metric value (else) + if (currentSystem.equals(getString(R.string.us_system))){ + usSystemRadioBtn.setChecked(true); + }else{ + metricSysRadioBtn.setChecked(true); + } + + //check which pay mode for reports to be used + String reportPayMode = sharedPreferences.getString("reportSTEEMPayMode",sbdSPPay); + if (reportPayMode.equals(fullSPPay)){ + fullSPayRadioBtn.setChecked(true); + }else{ + sbdSPPayRadioBtn.setChecked(true); + } + + + //check which data source is active now + + String dataTrackingSystem = sharedPreferences.getString("dataTrackingSystem",""); + if (dataTrackingSystem.equals(getString(R.string.fitbit_tracking))){ + fitbitBtn.setChecked(true); + + //also hide aggressive mode if fitbit is on, and show fitbit configuration + aggModeSection.setVisibility(View.INVISIBLE); + fitbitSettingsSection.setVisibility(View.VISIBLE); + }else{ + deviceSensorsBtn.setChecked(true); + //alternatively hide fitbit settings and show aggressive mode settings + aggModeSection.setVisibility(View.VISIBLE); + fitbitSettingsSection.setVisibility(View.INVISIBLE); + } + + RadioGroup trackingModeRadiogroup = findViewById(R.id.tracking_mode_radiogroup); + + //capture change event for radiobutton group to reflect on user available options + trackingModeRadiogroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + + if (deviceSensorsBtn.isChecked()){ + aggModeSection.setVisibility(View.VISIBLE); + fitbitSettingsSection.setVisibility(View.INVISIBLE); + }else{ + aggModeSection.setVisibility(View.INVISIBLE); + fitbitSettingsSection.setVisibility(View.VISIBLE); + } + } + }); + + //grab aggressive mode setting and update checkbox accordingly + String aggModeEnabled = sharedPreferences.getString("aggressiveBackgroundTracking",getString(R.string.aggr_back_tracking_off)); + Log.d(MainActivity.TAG,">>>>[Actifit] Agg Mode:"+aggModeEnabled); + Log.d(MainActivity.TAG,">>>>[Actifit] Agg Mode Test:"+aggModeEnabled.equals(getString(R.string.aggr_back_tracking_on))); + + aggBgTrackingChckBox.setChecked(aggModeEnabled.equals(getString(R.string.aggr_back_tracking_on))); + + //grab fitbit setting and update checkbox accordingly + String fitbitMeasurements = sharedPreferences.getString("fitbitMeasurements",getString(R.string.fitbit_measurements_on)); + fitbitMeasurementsChckBox.setChecked(fitbitMeasurements.equals(getString(R.string.fitbit_measurements_on))); + + + final Activity currentActivity = this; + + //need to update the info based on charity selection + charitySelected.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + TextView charityInfo = findViewById(R.id.charity_info); + Spinner charitySelected = findViewById(R.id.charity_options); + + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + String fullUrl = getString(R.string.steemit_url)+'@'+((Charity)charitySelected.getSelectedItem()).getCharityName(); + charityInfo.setText(fullUrl); + charityInfo.setMovementMethod(LinkMovementMethod.getInstance()); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + charityInfo.setText(""); + } + }); + + + Button BtnSaveSettings = findViewById(R.id.btn_save_settings); + BtnSaveSettings.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View arg0) { + //need to adjust the selection of the sensors and store it + + //store as new preferences + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + + //test for which option the user has set + if (metricSysRadioBtn.isChecked()) { + editor.putString("activeSystem", getString(R.string.metric_system)); + }else{ + editor.putString("activeSystem", getString(R.string.us_system)); + } + + //store selected STEEM pay mode + + //check which pay mode for reports to be used and store it + if (fullSPayRadioBtn.isChecked()){ + editor.putString("reportSTEEMPayMode", fullSPPay); + }else{ + editor.putString("reportSTEEMPayMode", sbdSPPay); + } + + //store selected tracking system + if (fitbitBtn.isChecked()) { + editor.putString("dataTrackingSystem", getString(R.string.fitbit_tracking)); + + //also deactivate running sensors if any instance is running + try { + ActivityMonitorService mSensorService = MainActivity.getmSensorService(); + if (mSensorService != null) { + stopService(MainActivity.getmServiceIntent()); + } + }catch(Exception e){ + e.printStackTrace(); + } + }else{ + editor.putString("dataTrackingSystem", getString(R.string.device_tracking)); + } + + + //PowerManager pm = ActivityMonitorService.getPowerManagerInstance(); + PowerManager.WakeLock wl = ActivityMonitorService.getWakeLockInstance(); + + //we need to enable aggressive checking only if device sensors are functioning, + //otherwise it's pointless + if (aggBgTrackingChckBox.isChecked()){ + editor.putString("aggressiveBackgroundTracking", getString(R.string.aggr_back_tracking_on)); + + }else{ + editor.putString("aggressiveBackgroundTracking", getString(R.string.aggr_back_tracking_off)); + //enable wake lock to ensure tracking functions in the background + if (wl!=null && wl.isHeld()) { + Log.d(MainActivity.TAG,">>>>[Actifit]Settings AGG MODE OFF"); + wl.release(); + } + } + + //reset value first + editor.putString("selectedCharity", ""); + + //check if charity mode is on and a charity has been selected + if (donateCharityChckBox.isChecked()){ + Spinner charitySelected = findViewById(R.id.charity_options); + if (charitySelected.getSelectedItem() !=null){ + editor.putString("selectedCharity", ((Charity)charitySelected.getSelectedItem()).getCharityName()); + editor.putString("selectedCharityDisplayName", charitySelected.getSelectedItem().toString()); + } + } + + //unset alarm and the need to restart Actifit notification reminder after reboot + alarmManager = (AlarmManager) getApplicationContext() + .getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(getApplicationContext(), ReminderNotificationService.class); + alarmIntent = PendingIntent.getService(getApplicationContext() + , ReminderNotificationService.NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + //unset any existing alarms first + alarmManager.cancel(alarmIntent); + + //check if reminder setting is on + if (reminderSetChckBox.isChecked()) { + editor.putString("selectedReminderHour", "" + hourOptions.getValue()); + editor.putString("selectedReminderMin", "" + minOptions.getValue()); + + //set the alarm at user defined value + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(System.currentTimeMillis()); + calendar.set(Calendar.HOUR_OF_DAY, hourOptions.getValue()); + calendar.set(Calendar.MINUTE, minOptions.getValue()); + + //PendingIntent.getService(currentActivity, ReminderNotificationService.NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); + intent.putExtra("NOTIFICATION_ID", ReminderNotificationService.NOTIFICATION_ID); + + Log.d(MainActivity.TAG,">>>>[Actifit]: set alarm manager"+hourOptions.getValue()+" "+minOptions.getValue()); + + alarmIntent = PendingIntent.getBroadcast(getApplicationContext() + , 0, intent, 0); + + alarmManager = (AlarmManager) getApplicationContext() + .getSystemService(Context.ALARM_SERVICE); + + //specify alarm interval to be every 24 hours at user defined slot + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), + 1000 * 60 * 60 * 24, alarmIntent); + } + + //store fitbit setting to see if user wants to grab measurements too + CheckBox fitbitMeasurements = findViewById(R.id.fitbit_measurements); + if (fitbitMeasurements.isChecked()){ + editor.putString("fitbitMeasurements", getString(R.string.fitbit_measurements_on)); + }else{ + editor.putString("fitbitMeasurements", getString(R.string.fitbit_measurements_off)); + } + + editor.apply(); + + currentActivity.finish(); + + } + }); + + //grab charity list + // Instantiate the RequestQueue. + RequestQueue queue = Volley.newRequestQueue(this); + + // This holds the url to connect to the API and grab the balance. + String charityUrl = getString(R.string.charity_list_api_url); + + JsonArrayRequest charitiesRequest = new JsonArrayRequest(Request.Method.GET, + charityUrl, null, new Response.Listener(){ + + @Override + public void onResponse(JSONArray transactionListArray) { + + ArrayList transactionList = new ArrayList(); + Spinner charityOptions = findViewById(R.id.charity_options); + // Handle the result + try { + + for (int i = 0; i < transactionListArray.length(); i++) { + // Retrieve each JSON object within the JSON array + JSONObject jsonObject = transactionListArray.getJSONObject(i); + + // Adds strings from the current object to the data string + transactionList.add(new Charity(jsonObject.getString("charity_name"), jsonObject.getString("display_name"))); + } + // convert content to adapter display, and render it + ArrayAdapter arrayAdapter = + new ArrayAdapter(getApplicationContext(),android.R.layout.simple_list_item_1, transactionList ){ + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent){ + // Get the Item from ListView + View view = super.getView(position, convertView, parent); + + // Initialize a TextView for ListView each Item + TextView tv = view.findViewById(android.R.id.text1); + + // Set the text color of TextView (ListView Item) + tv.setTextColor(Color.BLACK); + tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f); + + // Generate ListView Item using TextView + return view; + } + }; + + charityOptions.setAdapter(arrayAdapter); + + //choose a charity if one is already selected before + + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + + String currentCharity = (sharedPreferences.getString("selectedCharity","")); + String currentCharityDisplayName = (sharedPreferences.getString("selectedCharityDisplayName","")); + + if (!currentCharity.equals("")){ + Spinner charitySelected = findViewById(R.id.charity_options); + TextView charityInfo = findViewById(R.id.charity_info); + + donateCharityChckBox.setChecked(true); + charitySelected.setSelection(arrayAdapter.getPosition(new Charity(currentCharity,currentCharityDisplayName))); + String fullUrl = getString(R.string.steemit_url)+'@'+((Charity)charitySelected.getSelectedItem()).getCharityName(); + charityInfo.setText(fullUrl); + charityInfo.setMovementMethod(LinkMovementMethod.getInstance()); + } + + //actifitTransactions.setText("Response is: "+ response); + }catch (Exception e) { + Log.d(MainActivity.TAG,">>>>[Actifit]: Volley error"+e.getMessage()); + e.printStackTrace(); + } + + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Log.d(MainActivity.TAG,">>>>[Actifit]: Volley response error"+error.getMessage()); + error.printStackTrace(); + } + }); + + // Add charities request to be processed + queue.add(charitiesRequest); + + + //set proper reminder times + + hourOptions = findViewById(R.id.reminder_hour_options); + + hourOptions.setMinValue(0); + hourOptions.setMaxValue(23); + //hourOptions.setWrapSelectorWheel(false); + + minOptions = findViewById(R.id.reminder_min_options); + + minOptions.setMinValue(0); + minOptions.setMaxValue(59); + //minOptions.setWrapSelectorWheel(false); + + //formatting display of reminder times to add extra left zeros (hours and mins) + NumberPicker.Formatter formatter = new NumberPicker.Formatter(){ + @Override + public String format(int i) { + if (i<10){ + return "0"+i; + } + return ""+i; + } + }; + + hourOptions.setFormatter(formatter); + minOptions.setFormatter(formatter); + + //get pre-saved values for reminder setting + String reminderHour = (sharedPreferences.getString("selectedReminderHour","")); + String reminderMin = (sharedPreferences.getString("selectedReminderMin","")); + + //check which is the current active system + //if the setting is manually set as US System or default Metric value (else) + if (!reminderHour.equals("") && !reminderMin.equals("")){ + try { + hourOptions.setValue(Integer.parseInt(reminderHour)); + minOptions.setValue(Integer.parseInt(reminderMin)); + //we were able to grab proper values, set as checked + reminderSetChckBox.setChecked(true); + }catch(Exception e){ + e.printStackTrace(); + } + }else{ + metricSysRadioBtn.setChecked(true); + } + + } + + +} diff --git a/SimpleStepDetector.java b/SimpleStepDetector.java new file mode 100644 index 00000000..a4c6cf3e --- /dev/null +++ b/SimpleStepDetector.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013 Google Inc. + * + * 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 io.actifit.fitnesstracker.actifitfitnesstracker; + +public class SimpleStepDetector { + private static final int ACCEL_RING_SIZE = 50; + private static final int VEL_RING_SIZE = 10; + + // change this threshold according to your sensitivity preferences + private static final float STEP_THRESHOLD = 20f; + + private static final int STEP_DELAY_NS = 250000000; + + private int accelRingCounter = 0; + private float[] accelRingX = new float[ACCEL_RING_SIZE]; + private float[] accelRingY = new float[ACCEL_RING_SIZE]; + private float[] accelRingZ = new float[ACCEL_RING_SIZE]; + private int velRingCounter = 0; + private float[] velRing = new float[VEL_RING_SIZE]; + private long lastStepTimeNs = 0; + private float oldVelocityEstimate = 0; + + private StepListener listener; + + public void registerListener(StepListener listener) { + this.listener = listener; + } + + + public void updateAccel(long timeNs, float x, float y, float z) { + float[] currentAccel = new float[3]; + currentAccel[0] = x; + currentAccel[1] = y; + currentAccel[2] = z; + + // First step is to update our guess of where the global z vector is. + accelRingCounter++; + accelRingX[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[0]; + accelRingY[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[1]; + accelRingZ[accelRingCounter % ACCEL_RING_SIZE] = currentAccel[2]; + + float[] worldZ = new float[3]; + worldZ[0] = SensorFusionMath.sum(accelRingX) / Math.min(accelRingCounter, ACCEL_RING_SIZE); + worldZ[1] = SensorFusionMath.sum(accelRingY) / Math.min(accelRingCounter, ACCEL_RING_SIZE); + worldZ[2] = SensorFusionMath.sum(accelRingZ) / Math.min(accelRingCounter, ACCEL_RING_SIZE); + + float normalization_factor = SensorFusionMath.norm(worldZ); + + worldZ[0] = worldZ[0] / normalization_factor; + worldZ[1] = worldZ[1] / normalization_factor; + worldZ[2] = worldZ[2] / normalization_factor; + + // Next step is to figure out the component of the current acceleration + // in the direction of world_z and subtract gravity's contribution + float currentZ = SensorFusionMath.dot(worldZ, currentAccel) - normalization_factor; + velRingCounter++; + velRing[velRingCounter % VEL_RING_SIZE] = currentZ; + + float velocityEstimate = SensorFusionMath.sum(velRing); + + if (velocityEstimate > STEP_THRESHOLD && oldVelocityEstimate <= STEP_THRESHOLD + && (timeNs - lastStepTimeNs > STEP_DELAY_NS)) { + listener.step(timeNs); + lastStepTimeNs = timeNs; + } + oldVelocityEstimate = velocityEstimate; + } +} diff --git a/StepHistoryActivity.java b/StepHistoryActivity.java new file mode 100644 index 00000000..3aa87d67 --- /dev/null +++ b/StepHistoryActivity.java @@ -0,0 +1,94 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.content.Intent; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; + +public class StepHistoryActivity extends AppCompatActivity { + private ListView mStepsListView; + private StepsDBHelper mStepsDBHelper; + private ArrayList mStepCountList; + private ArrayList mStepFinalList; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_step_history); + + mStepsListView = findViewById(R.id.steps_list); + mStepFinalList = new ArrayList(); + + //grab the data to be displayed in the list + getDataForList(); + + //initializing date conversion components + String dateDisplay; + //existing date format + SimpleDateFormat dateFormIn = new SimpleDateFormat("yyyyMMdd"); + //output format + SimpleDateFormat dateFormOut = new SimpleDateFormat("MM/dd/yyyy"); + + //loop through the data to prepare it for proper display + for (int position=0;position arrayAdapter = + new ArrayAdapter(this,android.R.layout.simple_list_item_1, mStepFinalList); + + mStepsListView.setAdapter(arrayAdapter); + + //hook chart activity button + Button BtnViewChart = findViewById(R.id.chart_view); + + BtnViewChart.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View arg0) { + + Intent intent = new Intent(StepHistoryActivity.this, HistoryChartActivity.class); + startActivity(intent); + + } + }); + + } + + /** + * function handles preparing the proper data to the mStepCountList ArrayList + */ + public void getDataForList() { + mStepsDBHelper = new StepsDBHelper(this); + mStepCountList = mStepsDBHelper.readStepsEntries(); + } +} diff --git a/StepListener.java b/StepListener.java new file mode 100644 index 00000000..c609d09c --- /dev/null +++ b/StepListener.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013 Google Inc. + * + * 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 io.actifit.fitnesstracker.actifitfitnesstracker; + +/** + * Listens for alerts about steps being detected. + */ + +public interface StepListener { + /** + * Called when a step has been detected. Given the time in nanoseconds at + * which the step was detected. + */ + + public void step(long timeNs); +} diff --git a/StepsDBHelper.java b/StepsDBHelper.java new file mode 100644 index 00000000..4b93764a --- /dev/null +++ b/StepsDBHelper.java @@ -0,0 +1,223 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; + +public class StepsDBHelper extends SQLiteOpenHelper { + private static final int DATABASE_VERSION = 2; + private static final String DATABASE_NAME = "ActifitFitness"; + private static final String TABLE_STEPS_SUMMARY = "ActifitFitness"; + private static final String CREATION_DATE = "creationdate";//Date format is yyyyMMdd + private static final String STEPS_COUNT = "stepscount"; + private static final String TRACKING_DEVICE = "trackingdevice"; + + public static final String DEVICE_SENSORS = "Device Sensors"; + public static final String FITBIT = "Fitbit"; + + + private static final String CREATE_TABLE_ACTIFIT = "CREATE TABLE " + + TABLE_STEPS_SUMMARY + + "(" + CREATION_DATE + " INTEGER PRIMARY KEY," + + STEPS_COUNT + " INTEGER," + + TRACKING_DEVICE + " TEXT" + +")"; + + + StepsDBHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onUpgrade(SQLiteDatabase db,int oldVersion, int newVersion ){ + switch(oldVersion) { + // If the existing version is before v1 (on which we added the new field) + case 1: db.execSQL("ALTER TABLE "+TABLE_STEPS_SUMMARY + +" ADD COLUMN "+TRACKING_DEVICE+" TEXT"); + break; + default: + throw new IllegalStateException( + "onUpgrade() with unknown oldVersion " + oldVersion); + } + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_ACTIFIT); + + } + + /** + * function handles recording a step entry, and returning current step count + * this is the default function increasing by 1 + * @return + */ + public int createStepsEntry(){ + return createStepsEntry(1); + } + + /** + * function handles recording a step entry, and returning current step count + * @param incrementVal contains the amount to increase the count + * @return + */ + public int createStepsEntry(int incrementVal) + { + //grab step count for today, if exists + int todayStepCount = fetchTodayStepCount(); + String todaysDateString = getTodayProperFormat(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(CREATION_DATE, getTodayProperFormat()); + //if we found a match + if(todayStepCount>-1) + { + todayStepCount+=incrementVal; + values.put(STEPS_COUNT, todayStepCount); + values.put(TRACKING_DEVICE, DEVICE_SENSORS); + //updating entry with proper step count + db.update(TABLE_STEPS_SUMMARY, values, + CREATION_DATE + "=" + todaysDateString, null); + db.close(); + } + else + { + //create entry with 1 step count as first entry + todayStepCount = (incrementVal>-1?incrementVal:0); + values.put(STEPS_COUNT, todayStepCount); + values.put(TRACKING_DEVICE, DEVICE_SENSORS); + db.insert(TABLE_STEPS_SUMMARY, null, + values); + db.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + } + return todayStepCount; + } + + /** + * function handling grabbing the data and returning it + * @return ArrayList containing dates and steps + */ + public ArrayList readStepsEntries() + { + ArrayList mStepCountList = new ArrayList(); + //build up the query to grab all data + String selectQuery = "SELECT * FROM " + TABLE_STEPS_SUMMARY; + try { + //grab all entries + SQLiteDatabase db = this.getReadableDatabase(); + Cursor c = db.rawQuery(selectQuery, null); + //String priorDate = ""; + if (c.moveToFirst()) { + do { + DateStepsModel mDateStepsModel = new DateStepsModel(); + mDateStepsModel.mDate = c.getString((c.getColumnIndex(CREATION_DATE))); + mDateStepsModel.mStepCount = c.getInt((c.getColumnIndex(STEPS_COUNT))); + mDateStepsModel.mtrackingDevice = c.getString((c.getColumnIndex(TRACKING_DEVICE))); + //fix for the issue with multiple dates showing as row entries + //if (!mDateStepsModel.mDate.equals(priorDate)){ + //store the result only if this is a different display + mStepCountList.add(mDateStepsModel); + // } + } while (c.moveToNext()); + } + db.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return mStepCountList; + } + + /** + * function handles grabbing the current step count saved so far in case it was stored + * @return today's step count + */ + + public int fetchTodayStepCount() + { + //tracking found step count. Initiate at -1 to know if entry was found + int currentDateStepCounts = -1; + + //generate format for today + Date todaysDate = new Date(); + SimpleDateFormat formatToDB = new SimpleDateFormat("yyyyMMdd"); + String todaysDateString = formatToDB.format(todaysDate); + + String selectQuery = "SELECT " + STEPS_COUNT + " FROM " + + TABLE_STEPS_SUMMARY + " WHERE " + CREATION_DATE +" = "+ todaysDateString + ""; + try { + + SQLiteDatabase db = this.getReadableDatabase(); + Cursor c = db.rawQuery(selectQuery, null); + if (c.moveToFirst()) { + do { + //grab the value returned matching today's date + currentDateStepCounts = + c.getInt((c.getColumnIndex(STEPS_COUNT))); + + //just need first instance + break; + } while (c.moveToNext()); + } + db.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return currentDateStepCounts; + } + + public String getTodayProperFormat(){ + //generate format for today + Date todaysDate = new Date(); + SimpleDateFormat formatToDB = new SimpleDateFormat("yyyyMMdd"); + String todaysDateString = formatToDB.format(todaysDate); + return todaysDateString; + } + + /** + * function handles manually storing a step entry, to be used by alternative data + * tracking services + * @param activityCount contains the amount of activity to store on current date + * @return operation was successful (TRUE) or not (FALSE) + */ + public boolean manualInsertStepsEntry(int activityCount) + { + + String todaysDateString = getTodayProperFormat(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(CREATION_DATE, getTodayProperFormat()); + + values.put(STEPS_COUNT, activityCount); + values.put(TRACKING_DEVICE, FITBIT); + //updating entry with proper step count + int updatedRecords = db.update(TABLE_STEPS_SUMMARY, values, + CREATION_DATE + "=" + todaysDateString, null); + + //if no records updated, create a new entry + if (updatedRecords<1) { + db.insert(TABLE_STEPS_SUMMARY, null, + values); + } + db.close(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + +} diff --git a/WalletActivity.java b/WalletActivity.java new file mode 100644 index 00000000..5ec26db1 --- /dev/null +++ b/WalletActivity.java @@ -0,0 +1,268 @@ +package io.actifit.fitnesstracker.actifitfitnesstracker; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonArrayRequest; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class WalletActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_wallet); + + //grab links to layout items for later use + final TextView steemitUsername = findViewById(R.id.steemit_username); + Button BtnCheckBalance = findViewById(R.id.btn_get_balance); + + //try to check first if we had a user defined already and saved to preferences + //retrieving account data for simple reuse. Data is not stored anywhere outside actifit App. + SharedPreferences sharedPreferences = getSharedPreferences("actifitSets",MODE_PRIVATE); + // SharedPreferences.Editor editor = sharedPreferences.edit(); + + //grab stored value, if any + String curUser = sharedPreferences.getString("actifitUser",""); + steemitUsername.setText(curUser); + + final Activity callerActivity = this; + final Context callerContext = this; + + //make sure we have a value, and if so, automatically grab it + if (!curUser.equals("")) { + //if we already have data, emulate a click to grab the info + loadAccountBalance(steemitUsername, callerActivity, callerContext); + } + + //handle activity to fetch balance + BtnCheckBalance.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View arg0) { + + loadAccountBalance(steemitUsername, callerActivity, callerContext); + + } + }); + } + + void loadAccountBalance(TextView steemitUsername, Activity callerActivity, Context callerContext){ + //make sure we have a value, and if so, automatically grab it + if (!steemitUsername.getText().equals("")) { + //skip on spaces, upper case, and @ symbols to properly match steem username patterns + String username = steemitUsername.getText().toString() + .trim().toLowerCase().replace("@",""); + //connect to the interface to display result + final TextView actifitBalance = findViewById(R.id.actifit_balance); + final TextView actifitBalanceLbl = findViewById(R.id.actifit_balance_lbl); + final TextView actifitTransactionsLbl = findViewById(R.id.actifit_transactions_lbl); + final ListView actifitTransactionsView = findViewById(R.id.actifit_transactions); + final TextView actifitTransactionsError = findViewById(R.id.actifit_transactions_error); + + //hide if this is a recurring call + actifitTransactionsError.setVisibility(View.INVISIBLE); + + //initialize progress dialog + final ProgressDialog progress = new ProgressDialog(callerContext); + + // Instantiate the RequestQueue. + RequestQueue queue = Volley.newRequestQueue(callerActivity); + + // This holds the url to connect to the API and grab the balance. + // We append to it the username + String balanceUrl = getString(R.string.user_balance_api_url)+username; + + //display header + actifitBalanceLbl.setVisibility(View.VISIBLE); + // Request the balance of the user while expecting a JSON response + JsonObjectRequest balanceRequest = new JsonObjectRequest + (Request.Method.GET, balanceUrl, null, new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + //hide dialog + progress.hide(); + // Display the result + try { + //grab current token count + actifitBalance.setText(" " + response.getString("tokens")); + }catch(JSONException e){ + //hide dialog + progress.hide(); + actifitBalance.setText("Unable to fetch balance"); + } + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + //hide dialog + progress.hide(); + actifitBalance.setText("Unable to fetch balance"); + } + }); + + // Add balance request to be processed + queue.add(balanceRequest); + + // This holds the url to connect to the API and grab the transactions. + // We append to it the username + String transactionUrl = getString(R.string.user_transactions_api_url)+username; + + //display header + actifitTransactionsLbl.setVisibility(View.VISIBLE); + + // Request the transactions of the user first via JsonArrayRequest + // according to our data format + JsonArrayRequest transactionRequest = new JsonArrayRequest(Request.Method.GET, + transactionUrl, null, new Response.Listener(){ + + @Override + public void onResponse(JSONArray transactionListArray) { + //hide dialog + progress.hide(); + + ArrayList transactionList = new ArrayList(); + // Handle the result + try { + + for (int i = 0; i < transactionListArray.length(); i++) { + // Retrieve each JSON object within the JSON array + JSONObject jsonObject = transactionListArray.getJSONObject(i); + + // Build output + String transactionString = ""; + // Capture individual values + transactionString += jsonObject.has("reward_activity")?"Activity Type: "+jsonObject.getString("reward_activity") + "\n":""; + transactionString += jsonObject.has("token_count")?"Token Count: "+jsonObject.getString("token_count") + " AFIT(s)\n":""; + transactionString += jsonObject.has("date")?"Date Added: "+jsonObject.getString("date") + "\n":""; + //transactionString += jsonObject.has("url")?"Relevant Post: Post\n":""; + transactionString += jsonObject.has("note")?"Note: "+jsonObject.getString("note") + "\n":""; + /*String url = jsonObject.getString("url"); + + String note = jsonObject.getString("note");*/ + // Adds strings from the current object to the data string + transactionList.add(transactionString); + } + // convert content to adapter display, and render it + ArrayAdapter arrayAdapter = + new ArrayAdapter(getApplicationContext(),android.R.layout.simple_list_item_1, transactionList){ + @Override + public View getView(int position, View convertView, ViewGroup parent){ + // Get the Item from ListView + View view = super.getView(position, convertView, parent); + + // Initialize a TextView for ListView each Item + TextView tv = (TextView) view.findViewById(android.R.id.text1); + + // Set the text color of TextView (ListView Item) + tv.setTextColor(Color.BLACK); + tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f); + + // Generate ListView Item using TextView + return view; + } + }; + + actifitTransactionsView.setAdapter(arrayAdapter); + //actifitTransactions.setText("Response is: "+ response); + }catch (Exception e) { + //hide dialog + progress.hide(); + actifitTransactionsError.setVisibility(View.VISIBLE); + e.printStackTrace(); + } + + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + //hide dialog + progress.hide(); + //actifitTransactionsView.setText("Unable to fetch balance"); + actifitTransactionsError.setVisibility(View.VISIBLE); + } + }); + + + // Add transaction request to be processed + queue.add(transactionRequest); + + //display a progress dialog not to keep the user waiting + progress.setMessage(getString(R.string.fetching_user_balance)); + progress.show(); + + + }else{ + displayNotification(getString(R.string.username_missing),null, + callerContext, callerActivity, false); + + } + } + + void displayNotification(final String notification, final ProgressDialog progress, + final Context context, final Activity currentActivity, + final Boolean closeScreen){ + //render result + currentActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + //hide the progressDialog + if (progress!=null) { + progress.dismiss(); + } + + AlertDialog.Builder builder1 = new AlertDialog.Builder(context); + builder1.setMessage(notification); + + builder1.setCancelable(true); + + builder1.setPositiveButton( + "Dismiss", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + //if we need to close current Activity + if (closeScreen) { + //close current screen + Log.d(MainActivity.TAG,">>>Finish"); + currentActivity.finish(); + } + } + }); + //create and display alert window + AlertDialog alert11 = builder1.create(); + alert11.show(); + } + }); + + } + +}