diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 7e7f500..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index e30b29b..7318686 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -7,6 +7,7 @@
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 069b7f2..415d8e3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,7 +23,7 @@
+ android:value="com.samsung.health.step_count" />
() {
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Log.d("MainActivity", "Result $requestCode")
+ for (fragment in supportFragmentManager.primaryNavigationFragment!!.childFragmentManager.fragments) {
+ fragment.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt b/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt
new file mode 100644
index 0000000..ced0e76
--- /dev/null
+++ b/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt
@@ -0,0 +1,21 @@
+package com.dzeio.openhealth.connectors
+
+import android.app.Activity
+import android.content.Intent
+import com.dzeio.openhealth.data.weight.Weight
+
+interface ConnectorInterface {
+
+ val sourceID: String
+
+ /**
+ * Same as Activity/Fragment onRequestPermissionResult
+ *
+ * But it will only be launched if grantResults[0] == PackageManager.PERMISSION_GRANTED
+ */
+ fun onRequestPermissionResult(requestCode: Int, permission: Array, grantResult: IntArray)
+
+ fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
+
+ fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt b/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt
new file mode 100644
index 0000000..7d8f84a
--- /dev/null
+++ b/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt
@@ -0,0 +1,5 @@
+package com.dzeio.openhealth.connectors
+
+enum class DataType {
+ WEIGHT
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt
index b9f6231..c078755 100644
--- a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt
+++ b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt
@@ -2,108 +2,51 @@ package com.dzeio.openhealth.connectors
import android.Manifest
import android.app.Activity
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
-import com.dzeio.openhealth.data.AppDatabase
import com.dzeio.openhealth.data.weight.Weight
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.fitness.Fitness
import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.data.DataPoint
import com.google.android.gms.fitness.data.DataSet
-import com.google.android.gms.fitness.data.DataSource
import com.google.android.gms.fitness.data.DataType
import com.google.android.gms.fitness.request.DataReadRequest
-import com.google.android.gms.fitness.request.DataSourcesRequest
-import com.google.android.gms.fitness.request.OnDataPointListener
-import com.google.android.gms.fitness.request.SensorRequest
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.*
import java.util.*
import java.util.concurrent.TimeUnit
-enum class ActionRequestCode {
- FIND_DATA_SOURCES
-}
-
class GoogleFit(
private val activity: Activity,
-) {
+) : ConnectorInterface {
companion object {
const val TAG = "GoogleFitConnector"
}
-// private val fitnessOptions = FitnessOptions.builder()
-// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_READ)
-// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_WRITE)
-//
-// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ)
-// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_WRITE)
-//
-// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_READ)
-// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_WRITE)
-//
-// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_READ)
-// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_WRITE)
-// .build()
+
+ override val sourceID: String = "GoogleFit"
private val fitnessOptions = FitnessOptions.builder()
-// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE)
-// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT)
-// .addDataType(DataType.TYPE_SLEEP_SEGMENT)
-// .addDataType(DataType.TYPE_CALORIES_EXPENDED)
-// .addDataType(DataType.TYPE_BASAL_METABOLIC_RATE)
-// .addDataType(DataType.TYPE_POWER_SAMPLE)
-// .addDataType(DataType.TYPE_HEART_RATE_BPM)
-// .addDataType(DataType.TYPE_LOCATION_SAMPLE)
.addDataType(DataType.TYPE_WEIGHT)
.build()
private val runningQOrLater =
- android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q
-
- // [START dataPointListener_variable_reference]
- // Need to hold a reference to this listener, as it's passed into the "unregister"
- // method in order to stop all sensors from sending data to this listener.
- private var dataPointListener: OnDataPointListener? = null
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
fun import() {
- checkPermissionsAndRun(ActionRequestCode.FIND_DATA_SOURCES)
+ checkPermissionsAndRun()
}
- private fun checkPermissionsAndRun(actionRequestCode: ActionRequestCode) {
+ private fun checkPermissionsAndRun() {
if (permissionApproved()) {
- signIn(actionRequestCode)
+ signIn(0)
} else {
- requestRuntimePermissions(actionRequestCode)
- }
- }
-
- private fun requestRuntimePermissions(requestCode: ActionRequestCode) {
- val shouldProvideRationale =
- ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION)
-
- // Provide an additional rationale to the user. This would happen if the user denied the
- // request previously, but didn't check the "Don't ask again" checkbox.
- requestCode.let {
- if (shouldProvideRationale) {
- Log.i(TAG, "Displaying permission rationale to provide additional context.")
-// ProgressDialog.show(activity, "Waiting for authorization...", "")
- ActivityCompat.requestPermissions(activity,
- arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
- requestCode.ordinal)
- } else {
- Log.i(TAG, "Requesting permission")
- // Request permission. It's possible this can be auto answered if device policy
- // sets the permission in a given state or the user denied the permission
- // previously and checked "Never ask again".
- ActivityCompat.requestPermissions(activity,
- arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
- requestCode.ordinal)
- }
+ requestRuntimePermissions()
}
}
@@ -118,15 +61,41 @@ class GoogleFit(
return approved
}
- fun signIn(requestCode: ActionRequestCode) {
- if (oAuthPermissionsApproved()) {
- performActionForRequestCode(requestCode)
+ private fun requestRuntimePermissions() {
+ val shouldProvideRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION)
+
+ // Provide an additional rationale to the user. This would happen if the user denied the
+ // request previously, but didn't check the "Don't ask again" checkbox.
+ if (shouldProvideRationale) {
+ Log.i(TAG, "Displaying permission rationale to provide additional context.")
+// ProgressDialog.show(activity, "Waiting for authorization...", "")
+ ActivityCompat.requestPermissions(activity,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ 0)
} else {
+ Log.i(TAG, "Requesting permission")
+ // Request permission. It's possible this can be auto answered if device policy
+ // sets the permission in a given state or the user denied the permission
+ // previously and checked "Never ask again".
+ ActivityCompat.requestPermissions(activity,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ 0)
+ }
+ }
+
+ private fun signIn(requestCode: Int) {
+ if (oAuthPermissionsApproved()) {
+ Log.d("GoogleFitImporter", "Starting Import")
+ internalImportWeight()
+ } else {
+ Log.d("GoogleFitImporter", "Requesting Permission")
requestCode.let {
GoogleSignIn.requestPermissions(
activity,
- it.ordinal,
+ it,
getGoogleAccount(), fitnessOptions)
+
}
}
}
@@ -141,103 +110,7 @@ class GoogleFit(
*/
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
- /**
- * Runs the desired method, based on the specified request code. The request code is typically
- * passed to the Fit sign-in flow, and returned with the success callback. This allows the
- * caller to specify which method, post-sign-in, should be called.
- *
- * @param requestCode The code corresponding to the action to perform.
- */
- fun performActionForRequestCode(requestCode: ActionRequestCode) = when (requestCode) {
- ActionRequestCode.FIND_DATA_SOURCES -> findFitnessDataSources()
- }
-
- /** Finds available data sources and attempts to register on a specific [DataType]. */
- private fun findFitnessDataSources() { // [START find_data_sources]
- // Note: Fitness.SensorsApi.findDataSources() requires the ACCESS_FINE_LOCATION permission.
- Fitness.getSensorsClient(activity, getGoogleAccount())
- .findDataSources(
- DataSourcesRequest.Builder()
- .setDataTypes(DataType.TYPE_LOCATION_SAMPLE)
- .setDataSourceTypes(DataSource.TYPE_RAW)
- .build())
- .addOnSuccessListener { dataSources ->
- for (dataSource in dataSources) {
- Log.i(TAG, "Data source found: $dataSource")
- Log.i(TAG, "Data Source type: " + dataSource.dataType.name)
- // Let's register a listener to receive Activity data!
- if (dataSource.dataType == DataType.TYPE_LOCATION_SAMPLE && dataPointListener == null) {
- Log.i(TAG, "Data source for LOCATION_SAMPLE found! Registering.")
- registerFitnessDataListener(dataSource, DataType.TYPE_LOCATION_SAMPLE)
- }
- }
- }
- .addOnFailureListener { e -> Log.e(TAG, "failed", e) }
- // [END find_data_sources]
- }
-
- /**
- * Registers a listener with the Sensors API for the provided [DataSource] and [DataType] combo.
- */
- private fun registerFitnessDataListener(dataSource: DataSource, dataType: DataType) {
- // [START register_data_listener]
- dataPointListener = OnDataPointListener { dataPoint ->
- for (field in dataPoint.dataType.fields) {
- val value = dataPoint.getValue(field)
- Log.i(TAG, "Detected DataPoint field: ${field.name}")
- Log.i(TAG, "Detected DataPoint value: $value")
- }
- }
- Fitness.getSensorsClient(activity, getGoogleAccount())
- .add(
- SensorRequest.Builder()
- .setDataSource(dataSource) // Optional but recommended for custom data sets.
- .setDataType(dataType) // Can't be omitted.
- .setSamplingRate(10, TimeUnit.SECONDS)
- .build(),
- dataPointListener!!
- )
- .addOnCompleteListener { task ->
- if (task.isSuccessful) {
- Log.i(TAG, "Listener registered!")
- } else {
- Log.e(TAG, "Listener not registered.", task.exception)
- }
- }
- // [END register_data_listener]
- }
-
- @RequiresApi(Build.VERSION_CODES.O)
- fun getHistory() {
-
- val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
- val now = Date()
- calendar.time = now
- val endTime = calendar.timeInMillis
- calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today
- val startTime = calendar.timeInMillis
- val readRequest = DataReadRequest.Builder()
- .aggregate(DataType.AGGREGATE_CALORIES_EXPENDED)
- .bucketByActivityType(1, TimeUnit.SECONDS)
- .setTimeRange(startTime, endTime, TimeUnit.SECONDS)
- .build()
-
- Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions))
- .readData(readRequest)
- .addOnSuccessListener { response ->
- // The aggregate query puts datasets into buckets, so flatten into a
- // single list of datasets
- for (dataSet in response.buckets.flatMap { it.dataSets }) {
- dumpDataSet(dataSet)
- }
- }
- .addOnFailureListener { e ->
- Log.w(TAG,"There was an error reading data from Google Fit", e)
- }
-
- }
-
- fun importWeight(callback : () -> Unit) {
+ private fun internalImportWeight() {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val now = Date()
@@ -257,7 +130,7 @@ class GoogleFit(
// In this example, it's very unlikely that the request is for several hundred
// datapoints each consisting of a few steps and a timestamp. The more likely
// scenario is wanting to see how many steps were walked per day, for 7 days.
-// .aggregate()
+// .aggregate(DataType.AGGREGATE_WEIGHT_SUMMARY)
.read(DataType.TYPE_WEIGHT)
// Analogous to a "Group By" in SQL, defines how data should be aggregated.
@@ -267,110 +140,68 @@ class GoogleFit(
// .bucketByActivityType(1, TimeUnit.SECONDS)
// .bucketBySession()
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
- .build(), callback)
+ .build())
}
- private fun runRequest(request: DataReadRequest, callback: () -> Unit) {
+ private fun runRequest(request: DataReadRequest) {
Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions))
.readData(request)
.addOnSuccessListener { response ->
// The aggregate query puts datasets into buckets, so flatten into a
// single list of datasets
- Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size}")
+ Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size} ${response.status}")
for (dataSet in response.dataSets) {
dumpDataSet(dataSet)
}
- for (dataSet in response.buckets.flatMap { it.dataSets }) {
- dumpDataSet(dataSet)
- }
- callback.invoke()
+// for (dataSet in response.buckets.flatMap { it.dataSets }) {
+// dumpDataSet(dataSet)
+// }
}
.addOnFailureListener { e ->
Log.w(TAG,"There was an error reading data from Google Fit", e)
- callback.invoke()
}
}
private fun dumpDataSet(dataSet: DataSet) {
Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}")
- for (dp in dataSet.dataPoints) {
+ dataSet.dataPoints.forEachIndexed { index, dp ->
val weight = Weight()
Log.i(TAG,"Data point:")
Log.i(TAG,"\tType: ${dp.dataType.name}")
Log.i(TAG,"\tStart: ${dp.getStartTimeString()}")
Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}")
- weight.timestamp = dp.getStartTime(TimeUnit.SECONDS)
+ weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS)
weight.source = "GoogleFit"
for (field in dp.dataType.fields) {
weight.weight = dp.getValue(field).asFloat()
Log.i(TAG,"\tField: ${field.name.toString()} Value: ${dp.getValue(field)}")
+ callback(weight, (index + 1) == dataSet.dataPoints.size)
}
- // AppDatabase.getInstance(activity).weightDao().insert(weight)
}
}
- fun DataPoint.getStartTimeString(): String =
- SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
- .format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L))
+ fun DataPoint.getStartTimeString(): String = Date(this.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString()
- fun DataPoint.getEndTimeString(): String =
- SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
- .format(Date(this.getEndTime(TimeUnit.SECONDS) * 1000L))
+ fun DataPoint.getEndTimeString(): String = Date(this.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString()
-
- /** Unregisters the listener with the Sensors API. */
- private fun unregisterFitnessDataListener() {
- if (dataPointListener == null) {
- // This code only activates one listener at a time. If there's no listener, there's
- // nothing to unregister.
- return
- }
- // [START unregister_data_listener]
- // Waiting isn't actually necessary as the unregister call will complete regardless,
- // even if called from within onStop, but a callback can still be added in order to
- // inspect the results.
- Fitness.getSensorsClient(activity, getGoogleAccount())
- .remove(dataPointListener!!)
- .addOnCompleteListener { task ->
- if (task.isSuccessful && task.result!!) {
- Log.i(TAG, "Listener was removed!")
- } else {
- Log.i(TAG, "Listener was not removed.")
- }
- }
- // [END unregister_data_listener]
+ override fun onRequestPermissionResult(
+ requestCode: Int,
+ permission: Array,
+ grantResult: IntArray
+ ) {
+ signIn(requestCode)
}
- /** Returns a [DataReadRequest] for all step count changes in the past week. */
- private fun queryFitnessData(): DataReadRequest {
- // [START build_read_data_request]
- // Setting a start and end date using a range of 1 week before this moment.
- val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
- val now = Date()
- calendar.time = now
- val endTime = calendar.timeInMillis
- calendar.add(Calendar.YEAR, -1)
- val startTime = calendar.timeInMillis
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ signIn(requestCode)
+ }
- val dateFormat = DateFormat.getDateInstance()
- Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}")
- Log.i(TAG, "Range End: ${dateFormat.format(endTime)}")
+ private lateinit var callback: (weight: Weight, end: Boolean) -> Unit
- return DataReadRequest.Builder()
- // The data request can specify multiple data types to return, effectively
- // combining multiple data queries into one call.
- // In this example, it's very unlikely that the request is for several hundred
- // datapoints each consisting of a few steps and a timestamp. The more likely
- // scenario is wanting to see how many steps were walked per day, for 7 days.
- .aggregate(DataType.TYPE_STEP_COUNT_DELTA)
- // Analogous to a "Group By" in SQL, defines how data should be aggregated.
- // bucketByTime allows for a time span, whereas bucketBySession would allow
- // bucketing by "sessions", which would need to be defined in code.
- .bucketByTime(1, TimeUnit.SECONDS)
-// .bucketBySession()
- .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
- .build()
+ override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) {
+ this.callback = callback
+ checkPermissionsAndRun()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt.old b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt.old
new file mode 100644
index 0000000..2d5e17c
--- /dev/null
+++ b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt.old
@@ -0,0 +1,376 @@
+package com.dzeio.openhealth.connectors
+
+import android.Manifest
+import android.app.Activity
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import com.dzeio.openhealth.data.AppDatabase
+import com.dzeio.openhealth.data.weight.Weight
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.fitness.Fitness
+import com.google.android.gms.fitness.FitnessOptions
+import com.google.android.gms.fitness.data.DataPoint
+import com.google.android.gms.fitness.data.DataSet
+import com.google.android.gms.fitness.data.DataSource
+import com.google.android.gms.fitness.data.DataType
+import com.google.android.gms.fitness.request.DataReadRequest
+import com.google.android.gms.fitness.request.DataSourcesRequest
+import com.google.android.gms.fitness.request.OnDataPointListener
+import com.google.android.gms.fitness.request.SensorRequest
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.time.*
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+enum class ActionRequestCode {
+ FIND_DATA_SOURCES
+}
+
+class GoogleFit(
+ private val activity: Activity,
+) {
+ companion object {
+ const val TAG = "GoogleFitConnector"
+ }
+// private val fitnessOptions = FitnessOptions.builder()
+// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_READ)
+// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_WRITE)
+//
+// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ)
+// .addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_WRITE)
+//
+// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_READ)
+// .addDataType(DataType.TYPE_HEIGHT, FitnessOptions.ACCESS_WRITE)
+//
+// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_READ)
+// .addDataType(DataType.TYPE_WEIGHT, FitnessOptions.ACCESS_WRITE)
+// .build()
+
+ private val fitnessOptions = FitnessOptions.builder()
+// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE)
+// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT)
+// .addDataType(DataType.TYPE_SLEEP_SEGMENT)
+// .addDataType(DataType.TYPE_CALORIES_EXPENDED)
+// .addDataType(DataType.TYPE_BASAL_METABOLIC_RATE)
+// .addDataType(DataType.TYPE_POWER_SAMPLE)
+// .addDataType(DataType.TYPE_HEART_RATE_BPM)
+// .addDataType(DataType.TYPE_LOCATION_SAMPLE)
+ .addDataType(DataType.TYPE_WEIGHT)
+ .build()
+
+ private val runningQOrLater =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+
+ // [START dataPointListener_variable_reference]
+ // Need to hold a reference to this listener, as it's passed into the "unregister"
+ // method in order to stop all sensors from sending data to this listener.
+ private var dataPointListener: OnDataPointListener? = null
+
+ fun import() {
+ checkPermissionsAndRun(ActionRequestCode.FIND_DATA_SOURCES)
+ }
+
+ private fun checkPermissionsAndRun(actionRequestCode: ActionRequestCode) {
+ if (permissionApproved()) {
+ signIn(actionRequestCode)
+ } else {
+ requestRuntimePermissions(actionRequestCode)
+ }
+ }
+
+ private fun requestRuntimePermissions(requestCode: ActionRequestCode) {
+ val shouldProvideRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION)
+
+ // Provide an additional rationale to the user. This would happen if the user denied the
+ // request previously, but didn't check the "Don't ask again" checkbox.
+ requestCode.let {
+ if (shouldProvideRationale) {
+ Log.i(TAG, "Displaying permission rationale to provide additional context.")
+// ProgressDialog.show(activity, "Waiting for authorization...", "")
+ ActivityCompat.requestPermissions(activity,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ requestCode.ordinal)
+ } else {
+ Log.i(TAG, "Requesting permission")
+ // Request permission. It's possible this can be auto answered if device policy
+ // sets the permission in a given state or the user denied the permission
+ // previously and checked "Never ask again".
+ ActivityCompat.requestPermissions(activity,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ requestCode.ordinal)
+ }
+ }
+ }
+
+ private fun permissionApproved(): Boolean {
+ val approved = if (runningQOrLater) {
+ PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
+ activity,
+ Manifest.permission.ACCESS_FINE_LOCATION)
+ } else {
+ true
+ }
+ return approved
+ }
+
+ fun signIn(requestCode: ActionRequestCode) {
+ if (oAuthPermissionsApproved()) {
+ performActionForRequestCode(requestCode)
+ } else {
+ requestCode.let {
+ GoogleSignIn.requestPermissions(
+ activity,
+ it.ordinal,
+ getGoogleAccount(), fitnessOptions)
+ }
+ }
+ }
+
+ private fun oAuthPermissionsApproved() = GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions)
+
+ /**
+ * Gets a Google account for use in creating the Fitness client. This is achieved by either
+ * using the last signed-in account, or if necessary, prompting the user to sign in.
+ * `getAccountForExtension` is recommended over `getLastSignedInAccount` as the latter can
+ * return `null` if there has been no sign in before.
+ */
+ private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
+
+ /**
+ * Runs the desired method, based on the specified request code. The request code is typically
+ * passed to the Fit sign-in flow, and returned with the success callback. This allows the
+ * caller to specify which method, post-sign-in, should be called.
+ *
+ * @param requestCode The code corresponding to the action to perform.
+ */
+ fun performActionForRequestCode(requestCode: ActionRequestCode) = when (requestCode) {
+ ActionRequestCode.FIND_DATA_SOURCES -> findFitnessDataSources()
+ }
+
+ /** Finds available data sources and attempts to register on a specific [DataType]. */
+ private fun findFitnessDataSources() { // [START find_data_sources]
+ // Note: Fitness.SensorsApi.findDataSources() requires the ACCESS_FINE_LOCATION permission.
+ Fitness.getSensorsClient(activity, getGoogleAccount())
+ .findDataSources(
+ DataSourcesRequest.Builder()
+ .setDataTypes(DataType.TYPE_LOCATION_SAMPLE)
+ .setDataSourceTypes(DataSource.TYPE_RAW)
+ .build())
+ .addOnSuccessListener { dataSources ->
+ for (dataSource in dataSources) {
+ Log.i(TAG, "Data source found: $dataSource")
+ Log.i(TAG, "Data Source type: " + dataSource.dataType.name)
+ // Let's register a listener to receive Activity data!
+ if (dataSource.dataType == DataType.TYPE_LOCATION_SAMPLE && dataPointListener == null) {
+ Log.i(TAG, "Data source for LOCATION_SAMPLE found! Registering.")
+ registerFitnessDataListener(dataSource, DataType.TYPE_LOCATION_SAMPLE)
+ }
+ }
+ }
+ .addOnFailureListener { e -> Log.e(TAG, "failed", e) }
+ // [END find_data_sources]
+ }
+
+ /**
+ * Registers a listener with the Sensors API for the provided [DataSource] and [DataType] combo.
+ */
+ private fun registerFitnessDataListener(dataSource: DataSource, dataType: DataType) {
+ // [START register_data_listener]
+ dataPointListener = OnDataPointListener { dataPoint ->
+ for (field in dataPoint.dataType.fields) {
+ val value = dataPoint.getValue(field)
+ Log.i(TAG, "Detected DataPoint field: ${field.name}")
+ Log.i(TAG, "Detected DataPoint value: $value")
+ }
+ }
+ Fitness.getSensorsClient(activity, getGoogleAccount())
+ .add(
+ SensorRequest.Builder()
+ .setDataSource(dataSource) // Optional but recommended for custom data sets.
+ .setDataType(dataType) // Can't be omitted.
+ .setSamplingRate(10, TimeUnit.SECONDS)
+ .build(),
+ dataPointListener!!
+ )
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ Log.i(TAG, "Listener registered!")
+ } else {
+ Log.e(TAG, "Listener not registered.", task.exception)
+ }
+ }
+ // [END register_data_listener]
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun getHistory() {
+
+ val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+ val now = Date()
+ calendar.time = now
+ val endTime = calendar.timeInMillis
+ calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today
+ val startTime = calendar.timeInMillis
+ val readRequest = DataReadRequest.Builder()
+ .aggregate(DataType.AGGREGATE_CALORIES_EXPENDED)
+ .bucketByActivityType(1, TimeUnit.SECONDS)
+ .setTimeRange(startTime, endTime, TimeUnit.SECONDS)
+ .build()
+
+ Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions))
+ .readData(readRequest)
+ .addOnSuccessListener { response ->
+ // The aggregate query puts datasets into buckets, so flatten into a
+ // single list of datasets
+ for (dataSet in response.buckets.flatMap { it.dataSets }) {
+ dumpDataSet(dataSet)
+ }
+ }
+ .addOnFailureListener { e ->
+ Log.w(TAG,"There was an error reading data from Google Fit", e)
+ }
+
+ }
+
+ fun importWeight(callback : () -> Unit) {
+
+ val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+ val now = Date()
+ calendar.time = now
+ val endTime = calendar.timeInMillis
+ calendar.set(Calendar.YEAR, 2013) // Set year to 2013 to be sure to get data from when Google Fit Started to today
+ val startTime = calendar.timeInMillis
+
+ val dateFormat = DateFormat.getDateInstance()
+ Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}")
+ Log.i(TAG, "Range End: ${dateFormat.format(endTime)}")
+
+
+ runRequest(DataReadRequest.Builder()
+ // The data request can specify multiple data types to return, effectively
+ // combining multiple data queries into one call.
+ // In this example, it's very unlikely that the request is for several hundred
+ // datapoints each consisting of a few steps and a timestamp. The more likely
+ // scenario is wanting to see how many steps were walked per day, for 7 days.
+// .aggregate()
+ .read(DataType.TYPE_WEIGHT)
+
+ // Analogous to a "Group By" in SQL, defines how data should be aggregated.
+ // bucketByTime allows for a time span, whereas bucketBySession would allow
+ // bucketing by "sessions", which would need to be defined in code.
+// .bucketByTime(1, TimeUnit.MINUTES)
+// .bucketByActivityType(1, TimeUnit.SECONDS)
+// .bucketBySession()
+ .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
+ .build(), callback)
+
+ }
+
+ private fun runRequest(request: DataReadRequest, callback: () -> Unit) {
+ Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions))
+ .readData(request)
+ .addOnSuccessListener { response ->
+ // The aggregate query puts datasets into buckets, so flatten into a
+ // single list of datasets
+ Log.d(TAG, "Received response! ${response.dataSets.size} ${response.buckets.size}")
+ for (dataSet in response.dataSets) {
+ dumpDataSet(dataSet)
+ }
+ for (dataSet in response.buckets.flatMap { it.dataSets }) {
+ dumpDataSet(dataSet)
+ }
+ callback.invoke()
+ }
+ .addOnFailureListener { e ->
+ Log.w(TAG,"There was an error reading data from Google Fit", e)
+ callback.invoke()
+ }
+ }
+
+ private fun dumpDataSet(dataSet: DataSet) {
+ Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}")
+ for (dp in dataSet.dataPoints) {
+ val weight = Weight()
+ Log.i(TAG,"Data point:")
+ Log.i(TAG,"\tType: ${dp.dataType.name}")
+ Log.i(TAG,"\tStart: ${dp.getStartTimeString()}")
+ Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}")
+ weight.timestamp = dp.getStartTime(TimeUnit.SECONDS)
+ weight.source = "GoogleFit"
+ for (field in dp.dataType.fields) {
+ weight.weight = dp.getValue(field).asFloat()
+ Log.i(TAG,"\tField: ${field.name.toString()} Value: ${dp.getValue(field)}")
+ }
+ // AppDatabase.getInstance(activity).weightDao().insert(weight)
+ }
+ }
+
+ fun DataPoint.getStartTimeString(): String =
+ SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
+ .format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L))
+
+ fun DataPoint.getEndTimeString(): String =
+ SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
+ .format(Date(this.getEndTime(TimeUnit.SECONDS) * 1000L))
+
+
+ /** Unregisters the listener with the Sensors API. */
+ private fun unregisterFitnessDataListener() {
+ if (dataPointListener == null) {
+ // This code only activates one listener at a time. If there's no listener, there's
+ // nothing to unregister.
+ return
+ }
+ // [START unregister_data_listener]
+ // Waiting isn't actually necessary as the unregister call will complete regardless,
+ // even if called from within onStop, but a callback can still be added in order to
+ // inspect the results.
+ Fitness.getSensorsClient(activity, getGoogleAccount())
+ .remove(dataPointListener!!)
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful && task.result!!) {
+ Log.i(TAG, "Listener was removed!")
+ } else {
+ Log.i(TAG, "Listener was not removed.")
+ }
+ }
+ // [END unregister_data_listener]
+ }
+
+ /** Returns a [DataReadRequest] for all step count changes in the past week. */
+ private fun queryFitnessData(): DataReadRequest {
+ // [START build_read_data_request]
+ // Setting a start and end date using a range of 1 week before this moment.
+ val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
+ val now = Date()
+ calendar.time = now
+ val endTime = calendar.timeInMillis
+ calendar.add(Calendar.YEAR, -1)
+ val startTime = calendar.timeInMillis
+
+ val dateFormat = DateFormat.getDateInstance()
+ Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}")
+ Log.i(TAG, "Range End: ${dateFormat.format(endTime)}")
+
+ return DataReadRequest.Builder()
+ // The data request can specify multiple data types to return, effectively
+ // combining multiple data queries into one call.
+ // In this example, it's very unlikely that the request is for several hundred
+ // datapoints each consisting of a few steps and a timestamp. The more likely
+ // scenario is wanting to see how many steps were walked per day, for 7 days.
+ .aggregate(DataType.TYPE_STEP_COUNT_DELTA)
+ // Analogous to a "Group By" in SQL, defines how data should be aggregated.
+ // bucketByTime allows for a time span, whereas bucketBySession would allow
+ // bucketing by "sessions", which would need to be defined in code.
+ .bucketByTime(1, TimeUnit.SECONDS)
+// .bucketBySession()
+ .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
+ .build()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt
index a6e080e..01e5501 100644
--- a/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt
+++ b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/SamsungHealth.kt
@@ -1,134 +1,107 @@
package com.dzeio.openhealth.connectors.samsunghealth
import android.app.Activity
-import android.app.AlertDialog
-import android.content.DialogInterface
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
import android.util.Log
+import com.dzeio.openhealth.MainActivity
+import com.dzeio.openhealth.connectors.ConnectorInterface
+import com.dzeio.openhealth.data.weight.Weight
import com.samsung.android.sdk.healthdata.*
+import com.samsung.android.sdk.healthdata.HealthConstants.StepCount
import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener
import com.samsung.android.sdk.healthdata.HealthPermissionManager.*
-import com.samsung.android.sdk.healthdata.HealthResultHolder.ResultListener
-import java.lang.Boolean
-import kotlin.Exception
+import java.util.*
class SamsungHealth(
private val context: Activity
-) {
+) : ConnectorInterface {
+
companion object {
const val TAG = "SamsungHealthConnector"
}
- private val store: HealthDataStore get() = _store!!
- private var _store: HealthDataStore? = null
- private val keySet: HashSet = HashSet()
- private val permissionListener: ResultListener =
- ResultListener { result ->
- Log.d(TAG, "Permission callback is received.")
- val resultMap: Map = result!!.resultMap
-
- if (resultMap.containsValue(Boolean.FALSE)) {
- // Requesting permission fails
- Log.d(TAG, "Requesting permission fails")
-
- } else {
- // Get the current step count and display it
- getStepCount()
- }
- }
-
- private val connectionListener: ConnectionListener = object : ConnectionListener {
+ private val listener = object : ConnectionListener {
override fun onConnected() {
- Log.d(TAG, "Health data service is connected.")
- val pmsManager = HealthPermissionManager(store)
- try {
- val resultMap = pmsManager.isPermissionAcquired(keySet)
-
- if (resultMap.containsValue(Boolean.FALSE)) {
- // Request the permission for reading step counts if it is not acquired
- pmsManager.requestPermissions(keySet, context)
- .setResultListener(permissionListener)
- } else {
- // Get the current step count and display it
- getStepCount()
- }
- } catch (e: Exception) {
- Log.e(TAG, e.javaClass.name + " - " + e.message)
- Log.e(TAG, "Permission setting fails.")
+ Log.d(TAG, "Connected!")
+ if (isPermissionAcquired()) {
+ reporter.start()
+ } else {
+ requestPermission()
}
}
-
- override fun onConnectionFailed(error: HealthConnectionErrorResult) {
+ override fun onConnectionFailed(p0: HealthConnectionErrorResult?) {
Log.d(TAG, "Health data service is not available.")
- showConnectionFailureDialog(error)
}
override fun onDisconnected() {
Log.d(TAG, "Health data service is disconnected.")
}
+
}
- init {
- //keySet = HashSet()
- keySet.add(PermissionKey(HealthConstants.Weight.HEALTH_DATA_TYPE, PermissionType.READ))
+ private val store : HealthDataStore = HealthDataStore(context, listener)
- _store = HealthDataStore(context, connectionListener)
+ private fun isPermissionAcquired(): Boolean {
+ val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ)
+ val pmsManager = HealthPermissionManager(store)
+ try {
+ // Check whether the permissions that this application needs are acquired
+ val resultMap = pmsManager.isPermissionAcquired(setOf(permKey))
+ return !resultMap.containsValue(java.lang.Boolean.FALSE)
+ } catch (e: java.lang.Exception) {
+ Log.e(TAG, "Permission request fails.", e)
+ }
+ return false
}
- fun test() {
+ private fun requestPermission() {
+ val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ)
+ val pmsManager = HealthPermissionManager(store)
+ try {
+ // Show user permission UI for allowing user to change options
+ pmsManager.requestPermissions(setOf(permKey), context)
+ .setResultListener { result: PermissionResult ->
+ Log.d(TAG, "Permission callback is received.")
+ val resultMap =
+ result.resultMap
+ if (resultMap.containsValue(java.lang.Boolean.FALSE)) {
+ Log.d(TAG, "No Data???")
+ } else {
+ // Get the current step count and display it
+ reporter.start()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Permission setting fails.", e)
+ }
+ }
+
+ private val stepCountObserver = object : StepCountReporter.StepCountObserver {
+ override fun onChanged(count: Int) {
+ Log.d(TAG, "Step reported : $count")
+ }
+ }
+
+ private val reporter = StepCountReporter(store, stepCountObserver, Handler(Looper.getMainLooper()))
+
+ /**
+ * Connector
+ */
+
+ override val sourceID: String = "SamsungHealth"
+
+ override fun onRequestPermissionResult(
+ requestCode: Int,
+ permission: Array,
+ grantResult: IntArray
+ ) {}
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
+
+ override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) {
store.connectService()
}
-
- private fun getStepCount() {
- val resolver = HealthDataResolver(store, null)
- val request = HealthDataResolver.ReadRequest.Builder()
- .setDataType(HealthConstants.Weight.HEALTH_DATA_TYPE)
- .setLocalTimeRange(HealthConstants.Weight.START_TIME, HealthConstants.Weight.TIME_OFFSET, 0, System.currentTimeMillis())
- .build()
-
- try {
- val res = resolver.read(request).await()
-
- res.use { res1 ->
- val iterator = res1.iterator()
- if (iterator.hasNext()) {
- val data = iterator.next()
- val value = data.getFloat(HealthConstants.Weight.WEIGHT)
- val time = data.getLong(HealthConstants.Weight.CREATE_TIME)
- Log.d(TAG, "Data received! $value $time")
- }
- }
- } catch (e : Exception) {
- Log.d(TAG, "Reading failed!")
- }
- }
-
- private fun showConnectionFailureDialog(error: HealthConnectionErrorResult) {
- val alert: AlertDialog.Builder = AlertDialog.Builder(context)
- var message = "Connection with Samsung Health is not available"
- if (error.hasResolution()) {
- message = when (error.errorCode) {
- HealthConnectionErrorResult.PLATFORM_NOT_INSTALLED -> "Please install Samsung Health"
- HealthConnectionErrorResult.OLD_VERSION_PLATFORM -> "Please upgrade Samsung Health"
- HealthConnectionErrorResult.PLATFORM_DISABLED -> "Please enable Samsung Health"
- HealthConnectionErrorResult.USER_AGREEMENT_NEEDED -> "Please agree with Samsung Health policy"
- else -> "Please make Samsung Health available"
- }
- }
- alert.setMessage(message)
- alert.setPositiveButton("OK", DialogInterface.OnClickListener { dialog, id ->
- if (error.hasResolution()) {
- error.resolve(context)
- }
- })
- if (error.hasResolution()) {
- alert.setNegativeButton("Cancel", null)
- }
- alert.show()
- }
-
-
- fun destroy() {
- store.disconnectService()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/StepCountReporter.kt b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/StepCountReporter.kt
new file mode 100644
index 0000000..4545915
--- /dev/null
+++ b/app/src/main/java/com/dzeio/openhealth/connectors/samsunghealth/StepCountReporter.kt
@@ -0,0 +1,88 @@
+package com.dzeio.openhealth.connectors.samsunghealth
+
+import android.os.Handler
+import android.util.Log
+import com.samsung.android.sdk.healthdata.HealthConstants.StepCount
+import com.samsung.android.sdk.healthdata.HealthData
+import com.samsung.android.sdk.healthdata.HealthDataObserver
+import com.samsung.android.sdk.healthdata.HealthDataResolver
+import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest
+import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateRequest.AggregateFunction
+import com.samsung.android.sdk.healthdata.HealthDataResolver.AggregateResult
+import com.samsung.android.sdk.healthdata.HealthDataStore
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+class StepCountReporter(
+ private val mStore: HealthDataStore, private val mStepCountObserver: StepCountObserver,
+ resultHandler: Handler?
+) {
+ private val mHealthDataResolver: HealthDataResolver
+ private val mHealthDataObserver: HealthDataObserver
+ fun start() {
+ // Register an observer to listen changes of step count and get today step count
+ HealthDataObserver.addObserver(mStore, StepCount.HEALTH_DATA_TYPE, mHealthDataObserver)
+ readTodayStepCount()
+ }
+
+ fun stop() {
+ HealthDataObserver.removeObserver(mStore, mHealthDataObserver)
+ }
+
+ // Read the today's step count on demand
+ private fun readTodayStepCount() {
+ // Set time range from start time of today to the current time
+ val startTime = getUtcStartOfDay(System.currentTimeMillis(), TimeZone.getDefault())
+ val endTime = startTime + TimeUnit.DAYS.toMillis(1)
+ val request = AggregateRequest.Builder()
+ .setDataType(StepCount.HEALTH_DATA_TYPE)
+ .addFunction(AggregateFunction.SUM, StepCount.COUNT, "total_step")
+ .setLocalTimeRange(StepCount.START_TIME, StepCount.TIME_OFFSET, startTime, endTime)
+ .build()
+ try {
+ mHealthDataResolver.aggregate(request)
+ .setResultListener { aggregateResult: AggregateResult ->
+ aggregateResult.use { result ->
+ val iterator: Iterator = result.iterator()
+ if (iterator.hasNext()) {
+ mStepCountObserver.onChanged(iterator.next().getInt("total_step"))
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("APP_TAG", "Getting step count fails.", e)
+ }
+ }
+
+ private fun getUtcStartOfDay(time: Long, tz: TimeZone): Long {
+ val cal = Calendar.getInstance(tz)
+ cal.timeInMillis = time
+ val year = cal[Calendar.YEAR]
+ val month = cal[Calendar.MONTH]
+ val date = cal[Calendar.DATE]
+ cal.timeZone = TimeZone.getTimeZone("UTC")
+ cal[Calendar.YEAR] = year
+ cal[Calendar.MONTH] = month
+ cal[Calendar.DATE] = date
+ cal[Calendar.HOUR_OF_DAY] = 0
+ cal[Calendar.MINUTE] = 0
+ cal[Calendar.SECOND] = 0
+ cal[Calendar.MILLISECOND] = 0
+ return cal.timeInMillis
+ }
+
+ interface StepCountObserver {
+ fun onChanged(count: Int)
+ }
+
+ init {
+ mHealthDataResolver = HealthDataResolver(mStore, resultHandler)
+ mHealthDataObserver = object : HealthDataObserver(resultHandler) {
+ // Update the step count when a change event is received
+ override fun onChange(dataTypeName: String) {
+ Log.d("APP_TAG", "Observer receives a data changed event")
+ readTodayStepCount()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt
index 4ec48f7..8809508 100644
--- a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt
+++ b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt
@@ -20,4 +20,7 @@ interface WeightDao : BaseDao {
@Query("Select * FROM Weight WHERE id=(SELECT max(id) FROM Weight)")
fun last(): Flow
+
+ @Query("DELETE FROM Weight where source = :source")
+ suspend fun deleteFromSource(source: String)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt
index ac68454..f17a720 100644
--- a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt
+++ b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightRepository.kt
@@ -15,4 +15,5 @@ class WeightRepository @Inject constructor(
suspend fun addWeight(weight: Weight) = weightDao.insert(weight)
suspend fun deleteWeight(weight: Weight) = weightDao.delete(weight)
+ suspend fun deleteFromSource(source: String) = weightDao.deleteFromSource(source)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportViewModel.kt
deleted file mode 100644
index f66033f..0000000
--- a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportViewModel.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.dzeio.openhealth.ui.main.import
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-//import com.dzeio.openhealth.connectors.GoogleFit
-
-class ImportViewModel : ViewModel() {
-
- val text = MutableLiveData().apply {
- value = "This is slideshow Fragment"
- }
- val importProgress = MutableLiveData().apply {
- value = 0
- }
- // If -1 progress is undetermined
- // If 0 no progress bar
- // Else progress bar
- val importProgressTotal = MutableLiveData().apply {
- value = 0
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt
similarity index 60%
rename from app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportFragment.kt
rename to app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt
index 6801fa6..813cdd2 100644
--- a/app/src/main/java/com/dzeio/openhealth/ui/main/import/ImportFragment.kt
+++ b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt
@@ -1,6 +1,7 @@
-package com.dzeio.openhealth.ui.main.import
+package com.dzeio.openhealth.ui.main.imports
import android.app.ProgressDialog
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@@ -9,53 +10,38 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.ViewModelProvider
-import com.dzeio.openhealth.connectors.ActionRequestCode
+import androidx.lifecycle.lifecycleScope
+import com.dzeio.openhealth.connectors.ConnectorInterface
import com.dzeio.openhealth.connectors.GoogleFit
//import com.dzeio.openhealth.connectors.GoogleFit
import com.dzeio.openhealth.connectors.samsunghealth.SamsungHealth
+import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.databinding.FragmentImportBinding
+import dagger.hilt.android.AndroidEntryPoint
-class ImportFragment : Fragment() {
+@AndroidEntryPoint
+class ImportFragment : BaseFragment(ImportViewModel::class.java) {
companion object {
const val TAG = "ImportFragment"
}
- private var _binding: FragmentImportBinding? = null
-
- // This property is only valid between onCreateView and
- // onDestroyView.
- private val binding get() = _binding!!
- private lateinit var viewModel: ImportViewModel
+ override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentImportBinding = FragmentImportBinding::inflate
private lateinit var progressDialog: ProgressDialog
- private lateinit var fit: GoogleFit
+ private lateinit var fit: ConnectorInterface
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- viewModel = ViewModelProvider(this).get(ImportViewModel::class.java)
-
- _binding = FragmentImportBinding.inflate(inflater, container, false)
- val root: View = binding.root
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
progressDialog = ProgressDialog(requireContext())
progressDialog.apply {
- setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
setCancelable(false)
+ setTitle("Importing from source...")
}
- return root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
binding.importGoogleFit.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
importFromGoogleFit()
@@ -66,29 +52,67 @@ class ImportFragment : Fragment() {
}
}
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-
- @RequiresApi(Build.VERSION_CODES.O)
- fun importFromGoogleFit() {
- viewModel.importProgressTotal.postValue(-1)
+ private fun importFromGoogleFit() {
+ progressDialog.show()
fit = GoogleFit(requireActivity())
- progressDialog.show()
- fit.import()
- fit.getHistory()
-// google.importWeight {
-// progressDialog.dismiss()
-// }
+ var imported = 0
+
+ lifecycleScope.launchWhenStarted {
+ viewModel.deleteFromSource(fit.sourceID)
+ }.invokeOnCompletion {
+ //progressDialog.show()
+ fit.importWeight { weight, end ->
+ Log.d("Importer", "Importing $weight")
+ weight.source = fit.sourceID
+ progressDialog.setTitle("Importing from source... ${++imported}")
+ lifecycleScope.launchWhenStarted {
+ viewModel.importWeight(weight)
+ }
+ if (end) {
+ Log.d("Importer", "Finished Importing")
+ progressDialog.dismiss()
+ return@importWeight
+ }
+
+ }
+ }
+
+
}
fun importFromSamsungHealth() {
- SamsungHealth(requireActivity())
- .test()
+ progressDialog.show()
+ fit = SamsungHealth(requireActivity())
+
+ var imported = 0
+
+ lifecycleScope.launchWhenStarted {
+ viewModel.deleteFromSource(fit.sourceID)
+ }.invokeOnCompletion {
+ //progressDialog.show()
+ fit.importWeight { weight, end ->
+ Log.d("Importer", "Importing $weight")
+ weight.source = fit.sourceID
+ progressDialog.setTitle("Importing from source... ${++imported}")
+ lifecycleScope.launchWhenStarted {
+ viewModel.importWeight(weight)
+ }
+ if (end) {
+ Log.d("Importer", "Finished Importing")
+ progressDialog.dismiss()
+ return@importWeight
+ }
+
+ }
+ }
}
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ fit.onActivityResult(requestCode, resultCode, data)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array,
grantResults: IntArray) {
when {
@@ -100,11 +124,7 @@ class ImportFragment : Fragment() {
grantResults[0] == PackageManager.PERMISSION_GRANTED -> {
Log.d(TAG, "Granted")
- // Permission was granted.
- val fitActionRequestCode = ActionRequestCode.values()[requestCode]
- fitActionRequestCode.let {
- fit.signIn(ActionRequestCode.FIND_DATA_SOURCES)
- }
+ fit.onRequestPermissionResult(requestCode, permissions, grantResults)
}
else -> {
// Permission denied.
diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportViewModel.kt
new file mode 100644
index 0000000..ed3651c
--- /dev/null
+++ b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportViewModel.kt
@@ -0,0 +1,32 @@
+package com.dzeio.openhealth.ui.main.imports
+
+import androidx.lifecycle.MutableLiveData
+import com.dzeio.openhealth.core.BaseViewModel
+import com.dzeio.openhealth.data.weight.Weight
+import com.dzeio.openhealth.data.weight.WeightRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.security.CodeSource
+import javax.inject.Inject
+
+
+@HiltViewModel
+class ImportViewModel @Inject internal constructor(
+ private val weightRepository: WeightRepository
+) : BaseViewModel() {
+
+ val text = MutableLiveData().apply {
+ value = "This is slideshow Fragment"
+ }
+ val importProgress = MutableLiveData().apply {
+ value = 0
+ }
+ // If -1 progress is undetermined
+ // If 0 no progress bar
+ // Else progress bar
+ val importProgressTotal = MutableLiveData().apply {
+ value = 0
+ }
+
+ suspend fun importWeight(weight: Weight) = weightRepository.addWeight(weight)
+ suspend fun deleteFromSource(source: String) = weightRepository.deleteFromSource(source)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt
index 24ff6d8..3f649d7 100644
--- a/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt
+++ b/app/src/main/java/com/dzeio/openhealth/ui/main/list_weight/ListWeightFragment.kt
@@ -10,11 +10,14 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.openhealth.adapters.WeightAdapter
import com.dzeio.openhealth.core.BaseFragment
+import com.dzeio.openhealth.data.weight.Weight
import com.dzeio.openhealth.databinding.FragmentListWeightBinding
import com.dzeio.openhealth.ui.dialogs.EditWeightDialog
import com.dzeio.openhealth.ui.main.home.HomeViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.collectLatest
+import java.util.*
@AndroidEntryPoint
class ListWeightFragment : BaseFragment(HomeViewModel::class.java) {
@@ -37,8 +40,10 @@ class ListWeightFragment : BaseFragment if (o1.timestamp > o2.timestamp) -1 else 1 }
+ adapter.set(itt)
}
}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
index 2b068d1..a0037ac 100644
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -4,27 +4,48 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+ android:pathData="M284,162H40M162,40V284"
+ android:strokeAlpha="0.5"
+ android:strokeLineJoin="round"
+ android:strokeWidth="76"
+ android:fillColor="#00000000"
+ android:strokeColor="#80E27E"
+ android:strokeLineCap="round"/>
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9..0000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_logo_app.xml b/app/src/main/res/drawable/ic_logo_app.xml
new file mode 100644
index 0000000..4460be8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_app.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo_app_white.xml b/app/src/main/res/drawable/ic_logo_app_white.xml
new file mode 100644
index 0000000..75a9d57
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_app_white.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo_background.xml b/app/src/main/res/drawable/ic_logo_background.xml
new file mode 100644
index 0000000..918db80
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_background.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml
index 433c21e..65f44b6 100644
--- a/app/src/main/res/layout/fragment_import.xml
+++ b/app/src/main/res/layout/fragment_import.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.main.import.ImportFragment">
+ tools:context=".ui.main.imports.ImportFragment">
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cf..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..28ad9c6
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a4b162c
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..da66b1b
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..df9c3f2
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..1ae011f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..ead7fc9
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..74ac628
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9bcd797
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..1487e3f
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..f6274b0
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index 78aaf0d..3ae8231 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -28,7 +28,7 @@
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file