From 39b79b752d821717fcdbf3a7c7dd08dcca7c6e3c Mon Sep 17 00:00:00 2001 From: Mohammad Farhat Date: Mon, 26 Nov 2018 15:22:01 +0200 Subject: [PATCH] Version 0.5.1 Updates: Coding Changes - Implement New Fitbit Indicator - Activity History - Implement STEEM Post Payout 50-50/Full SP - Implement Post to Steem Button Move - Implement Auto-save Post Content - Minor Enhancements & Bug Fixes --- ActivityMonitorService.java | 226 +++++++ Charity.java | 36 ++ DateStepsModel.java | 8 + HistoryChartActivity.java | 149 +++++ HttpResultHelper.java | 109 ++++ LeaderboardActivity.java | 196 ++++++ MainActivity.java | 690 ++++++++++++++++++++ MonitorRestartBroadcastReceiver.java | 17 + MultiSelectionSpinner.java | 192 ++++++ NxFitbitHelper.java | 189 ++++++ PostSteemitActivity.java | 918 +++++++++++++++++++++++++++ ReminderNotificationService.java | 85 +++ SensorFusionMath.java | 62 ++ SettingsActivity.java | 458 +++++++++++++ SimpleStepDetector.java | 82 +++ StepHistoryActivity.java | 94 +++ StepListener.java | 29 + StepsDBHelper.java | 223 +++++++ WalletActivity.java | 268 ++++++++ 19 files changed, 4031 insertions(+) create mode 100644 ActivityMonitorService.java create mode 100644 Charity.java create mode 100644 DateStepsModel.java create mode 100644 HistoryChartActivity.java create mode 100644 HttpResultHelper.java create mode 100644 LeaderboardActivity.java create mode 100644 MainActivity.java create mode 100644 MonitorRestartBroadcastReceiver.java create mode 100644 MultiSelectionSpinner.java create mode 100644 NxFitbitHelper.java create mode 100644 PostSteemitActivity.java create mode 100644 ReminderNotificationService.java create mode 100644 SensorFusionMath.java create mode 100644 SettingsActivity.java create mode 100644 SimpleStepDetector.java create mode 100644 StepHistoryActivity.java create mode 100644 StepListener.java create mode 100644 StepsDBHelper.java create mode 100644 WalletActivity.java 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(); + } + }); + + } + +}