mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-23 19:32:11 +00:00
Clean up
This commit is contained in:
parent
f694ebfa37
commit
e1f11e1ae2
@ -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" />
|
||||
|
@ -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) {}
|
||||
}
|
@ -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)
|
||||
}
|
@ -2,4 +2,15 @@ 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
|
||||
*/
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user