1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-23 11:22:10 +00:00
This commit is contained in:
Florian Bouillon 2021-12-21 18:05:05 +01:00
parent f694ebfa37
commit e1f11e1ae2
8 changed files with 172 additions and 141 deletions

View File

@ -2,8 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dzeio.openhealth">
<!-- Google Fit -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Phone Services -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION " />
<!-- Samsung Health-->
@ -25,6 +28,7 @@
android:name="com.samsung.android.health.permission.read"
android:value="com.samsung.health.step_count" />
<!-- Google Fit -->
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
@ -32,7 +36,6 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.OpenHealth.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -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<String>, 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) {}
}

View File

@ -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<String>, grantResult: IntArray)
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit)
}

View File

@ -3,3 +3,14 @@ package com.dzeio.openhealth.connectors
enum class DataType {
WEIGHT
}
/**
* STEP_COUNT_CUMULATIVE
* ACTIVITY_SEGMENT
* SLEEP_SEGMENT
* CALORIES_EXPENDED
* BASAL_METABOLIC_RATE
* POWER_SAMPLE
* HEART_RATE_BPM
* LOCATION_SAMPLE
*/

View File

@ -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<String>,
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)
}
}

View File

@ -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"

View File

@ -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")
}
}
}
}

View File

@ -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<ImportViewModel, FragmentImportBinding>(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<ImportViewModel, FragmentImportBinding>(Impo
}
fun importFromSamsungHealth() {
private fun importFromSamsungHealth() {
progressDialog.show()
fit = SamsungHealth(requireActivity())