diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 415d8e3..634afa5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,8 +2,11 @@ + + + @@ -25,6 +28,7 @@ android:name="com.samsung.android.health.permission.read" android:value="com.samsung.health.step_count" /> + @@ -32,7 +36,6 @@ diff --git a/app/src/main/java/com/dzeio/openhealth/connectors/Connector.kt b/app/src/main/java/com/dzeio/openhealth/connectors/Connector.kt new file mode 100644 index 0000000..2453d81 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/connectors/Connector.kt @@ -0,0 +1,31 @@ +package com.dzeio.openhealth.connectors + +import android.app.Activity +import android.content.Intent +import com.dzeio.openhealth.data.weight.Weight + +abstract class Connector { + + enum class Data { + WEIGHT, + STEPS + } + + abstract val sourceID: String + + open fun init(activity: Activity) {} + + /** + * Same as Activity/Fragment onRequestPermissionResult + * + * But it will only be launched if grantResults[0] == PackageManager.PERMISSION_GRANTED + */ + open fun onRequestPermissionResult(requestCode: Int, permission: Array, grantResult: IntArray) {} + + /** + * Same as Activity/Fragment onActivityResult + */ + open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} + + open 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/ConnectorInterface.kt b/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt deleted file mode 100644 index ced0e76..0000000 --- a/app/src/main/java/com/dzeio/openhealth/connectors/ConnectorInterface.kt +++ /dev/null @@ -1,21 +0,0 @@ -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 index 7d8f84a..b94ee19 100644 --- a/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt +++ b/app/src/main/java/com/dzeio/openhealth/connectors/DataType.kt @@ -2,4 +2,15 @@ package com.dzeio.openhealth.connectors enum class DataType { WEIGHT -} \ No newline at end of file +} + +/** + * STEP_COUNT_CUMULATIVE + * ACTIVITY_SEGMENT + * SLEEP_SEGMENT + * CALORIES_EXPENDED + * BASAL_METABOLIC_RATE + * POWER_SAMPLE + * HEART_RATE_BPM + * LOCATION_SAMPLE + */ \ 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 c078755..c5e36e3 100644 --- a/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt +++ b/app/src/main/java/com/dzeio/openhealth/connectors/GoogleFit.kt @@ -6,7 +6,6 @@ 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.weight.Weight import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -17,14 +16,12 @@ import com.google.android.gms.fitness.data.DataSet import com.google.android.gms.fitness.data.DataType import com.google.android.gms.fitness.request.DataReadRequest import java.text.DateFormat -import java.text.SimpleDateFormat -import java.time.* import java.util.* import java.util.concurrent.TimeUnit class GoogleFit( private val activity: Activity, -) : ConnectorInterface { +) : Connector() { companion object { const val TAG = "GoogleFitConnector" } @@ -33,25 +30,25 @@ class GoogleFit( private val fitnessOptions = FitnessOptions.builder() .addDataType(DataType.TYPE_WEIGHT) + .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE) .build() - private val runningQOrLater = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - - fun import() { - checkPermissionsAndRun() - } - - private fun checkPermissionsAndRun() { + private fun checkPermissionsAndRun(data: Data) { if (permissionApproved()) { - signIn(0) + signIn(data) } else { - requestRuntimePermissions() + Log.d(TAG, "Asking for permission") + // Ask for permission + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + data.ordinal + ) } } private fun permissionApproved(): Boolean { - val approved = if (runningQOrLater) { + val approved = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission( activity, Manifest.permission.ACCESS_FINE_LOCATION) @@ -61,147 +58,116 @@ class GoogleFit( return approved } - 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) { + private fun signIn(data: Data) { if (oAuthPermissionsApproved()) { - Log.d("GoogleFitImporter", "Starting Import") - internalImportWeight() + startImport(data) } else { - Log.d("GoogleFitImporter", "Requesting Permission") - requestCode.let { - GoogleSignIn.requestPermissions( - activity, - it, - getGoogleAccount(), fitnessOptions) - - } + Log.d("GoogleFitImporter", "Signing In") + GoogleSignIn.requestPermissions( + activity, + data.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) - private fun internalImportWeight() { - + private val timeRange by lazy { val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - val now = Date() - calendar.time = now + calendar.time = Date() 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 + + // Set year to 2013 to be sure to get data from when Google Fit Started to today + calendar.set(Calendar.YEAR, 2013) val startTime = calendar.timeInMillis + arrayOf(startTime, endTime) + } + + + private fun startImport(data: Data) { + Log.d("GoogleFitImporter", "Importing for ${data.name}") val dateFormat = DateFormat.getDateInstance() - Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}") - Log.i(TAG, "Range End: ${dateFormat.format(endTime)}") + Log.i(TAG, "Range Start: ${dateFormat.format(timeRange[0])}") + Log.i(TAG, "Range End: ${dateFormat.format(timeRange[1])}") + var type = DataType.TYPE_WEIGHT + var timeUnit = TimeUnit.MILLISECONDS + + when (data) { + Data.STEPS -> { + type = DataType.TYPE_STEP_COUNT_CUMULATIVE + } + else -> {} + } 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(DataType.AGGREGATE_WEIGHT_SUMMARY) - .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()) + .read(type) + .setTimeRange(timeRange[0], timeRange[1], timeUnit) + .build(), data) } - private fun runRequest(request: DataReadRequest) { + private fun runRequest(request: DataReadRequest, data: Data) { 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} ${response.status}") for (dataSet in response.dataSets) { - dumpDataSet(dataSet) + Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") + dataSet.dataPoints.forEachIndexed { index, dp -> + val isLast = (index + 1) == dataSet.dataPoints.size + + // Global + Log.i(TAG,"Importing Data point:") + Log.i(TAG,"\tType: ${dp.dataType.name}") + Log.i(TAG,"\tStart: ${dp.getStartTimeString()}") + Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}") + + // Field Specifics + for (field in dp.dataType.fields) { + Log.i(TAG,"\tField: ${field.name} Value: ${dp.getValue(field)}") + when (data) { + Data.WEIGHT -> { + val weight = Weight() + weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS) + weight.weight = dp.getValue(field).asFloat() + weightCallback(weight, isLast) + } + else -> {} + } + } + } } -// 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) + Log.e(TAG,"There was an error reading data from Google Fit", e) } } - private fun dumpDataSet(dataSet: DataSet) { - Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") - 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.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) - } - } - } + private fun DataPoint.getStartTimeString(): String = Date(this.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString() - fun DataPoint.getStartTimeString(): String = Date(this.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString() - - fun DataPoint.getEndTimeString(): String = Date(this.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString() + private fun DataPoint.getEndTimeString(): String = Date(this.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString() override fun onRequestPermissionResult( requestCode: Int, permission: Array, grantResult: IntArray ) { - signIn(requestCode) + signIn(Data.values()[requestCode]) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - signIn(requestCode) + signIn(Data.values()[requestCode]) } - private lateinit var callback: (weight: Weight, end: Boolean) -> Unit + private lateinit var weightCallback: (weight: Weight, end: Boolean) -> Unit override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) { - this.callback = callback - checkPermissionsAndRun() + this.weightCallback = callback + checkPermissionsAndRun(Data.WEIGHT) } } \ 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 01e5501..6838dbe 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 @@ -5,19 +5,20 @@ 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.connectors.Connector 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 java.util.* +/** + * Does not FUCKING work + */ class SamsungHealth( private val context: Activity -) : ConnectorInterface { +) : Connector() { companion object { const val TAG = "SamsungHealthConnector" diff --git a/app/src/main/java/com/dzeio/openhealth/services/StepCountService.kt b/app/src/main/java/com/dzeio/openhealth/services/StepCountService.kt new file mode 100644 index 0000000..856d081 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/services/StepCountService.kt @@ -0,0 +1,40 @@ +package com.dzeio.openhealth.services + +import android.app.job.JobParameters +import android.app.job.JobService +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log + +class StepCountService : JobService(), SensorEventListener { + override fun onStartJob(params: JobParameters?): Boolean { + Log.d("StepCountService", "Service Started") + val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager + val stepCountSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) + stepCountSensor.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL) + } + return true + } + + override fun onStopJob(params: JobParameters?): Boolean { + Log.d("StepCountService", "Service Stopped :(") + return true + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + Log.d("StepCountService", "Accuracy changed $sensor, $accuracy") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + Log.d("StepCountService", "Event Triggered: $it") + it.values.firstOrNull()?.let { value -> + Log.d("StepCountService", "Step Count $value") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt index 813cdd2..ba28b6c 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/main/imports/ImportFragment.kt @@ -11,7 +11,7 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.RequiresApi import androidx.lifecycle.lifecycleScope -import com.dzeio.openhealth.connectors.ConnectorInterface +import com.dzeio.openhealth.connectors.Connector import com.dzeio.openhealth.connectors.GoogleFit //import com.dzeio.openhealth.connectors.GoogleFit import com.dzeio.openhealth.connectors.samsunghealth.SamsungHealth @@ -30,7 +30,7 @@ class ImportFragment : BaseFragment(Impo private lateinit var progressDialog: ProgressDialog - private lateinit var fit: ConnectorInterface + private lateinit var fit: Connector override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -81,7 +81,7 @@ class ImportFragment : BaseFragment(Impo } - fun importFromSamsungHealth() { + private fun importFromSamsungHealth() { progressDialog.show() fit = SamsungHealth(requireActivity())