1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-23 19:32:11 +00:00

Finally have GoogleFit YaY

This commit is contained in:
Florian Bouillon 2021-12-20 23:19:13 +01:00
parent 4a8fd50f8d
commit 049fc18297
Signed by: Florian Bouillon
GPG Key ID: 50BD648F12C86AB6
47 changed files with 901 additions and 623 deletions

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="14e181ff" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-12-20T00:23:13.795732Z" />
</component>
</project>

1
.idea/misc.xml generated
View File

@ -7,6 +7,7 @@
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/layout/fragment_import.xml" value="0.1" />
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/menu/activity_main_drawer.xml" value="0.10989583333333333" />
<entry key="..\:/Git/Dzeio/OpenHealth/app/src/main/res/menu/main.xml" value="0.1875" />
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/drawable/ic_logo_app.xml" value="0.163" />
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/activity_main.xml" value="0.5" />
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/app_bar_main.xml" value="0.5192107995846313" />
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/content_main.xml" value="0.5135869565217391" />

View File

@ -23,7 +23,7 @@
<!-- Samsung Health-->
<meta-data
android:name="com.samsung.android.health.permission.read"
android:value="com.samsung.health.weight" />
android:value="com.samsung.health.step_count" />
<meta-data
android:name="com.google.android.gms.version"

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -65,6 +65,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
) {
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?) {

View File

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

View File

@ -0,0 +1,5 @@
package com.dzeio.openhealth.connectors
enum class DataType {
WEIGHT
}

View File

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

View File

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

View File

@ -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<PermissionKey> = HashSet()
private val permissionListener: ResultListener<PermissionResult> =
ResultListener<PermissionResult> { result ->
Log.d(TAG, "Permission callback is received.")
val resultMap: Map<PermissionKey, kotlin.Boolean> = 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)
Log.d(TAG, "Connected!")
if (isPermissionAcquired()) {
reporter.start()
} 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.")
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))
_store = HealthDataStore(context, connectionListener)
}
fun test() {
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()
private val store : HealthDataStore = HealthDataStore(context, listener)
private fun isPermissionAcquired(): Boolean {
val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ)
val pmsManager = HealthPermissionManager(store)
try {
val res = resolver.read(request).await()
// 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
}
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")
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.d(TAG, "Reading failed!")
Log.e(TAG, "Permission setting fails.", e)
}
}
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"
private val stepCountObserver = object : StepCountReporter.StepCountObserver {
override fun onChanged(count: Int) {
Log.d(TAG, "Step reported : $count")
}
}
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()
}
private val reporter = StepCountReporter(store, stepCountObserver, Handler(Looper.getMainLooper()))
fun destroy() {
store.disconnectService()
/**
* Connector
*/
override val sourceID: String = "SamsungHealth"
override fun onRequestPermissionResult(
requestCode: Int,
permission: Array<String>,
grantResult: IntArray
) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) {
store.connectService()
}
}

View File

@ -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<HealthData> = 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()
}
}
}
}

View File

@ -20,4 +20,7 @@ interface WeightDao : BaseDao<Weight> {
@Query("Select * FROM Weight WHERE id=(SELECT max(id) FROM Weight)")
fun last(): Flow<Weight?>
@Query("DELETE FROM Weight where source = :source")
suspend fun deleteFromSource(source: String)
}

View File

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

View File

@ -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<String>().apply {
value = "This is slideshow Fragment"
}
val importProgress = MutableLiveData<Int>().apply {
value = 0
}
// If -1 progress is undetermined
// If 0 no progress bar
// Else progress bar
val importProgressTotal = MutableLiveData<Int>().apply {
value = 0
}
}

View File

@ -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, FragmentImportBinding>(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<String>,
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.

View File

@ -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<String>().apply {
value = "This is slideshow Fragment"
}
val importProgress = MutableLiveData<Int>().apply {
value = 0
}
// If -1 progress is undetermined
// If 0 no progress bar
// Else progress bar
val importProgressTotal = MutableLiveData<Int>().apply {
value = 0
}
suspend fun importWeight(weight: Weight) = weightRepository.addWeight(weight)
suspend fun deleteFromSource(source: String) = weightRepository.deleteFromSource(source)
}

View File

@ -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, FragmentListWeightBinding>(HomeViewModel::class.java) {
@ -37,8 +40,10 @@ class ListWeightFragment : BaseFragment<HomeViewModel, FragmentListWeightBinding
recycler.adapter = adapter
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
viewModel.fetchWeights().collect {
adapter.set(it)
viewModel.fetchWeights().collectLatest {
val itt = it.toMutableList()
itt.sortWith { o1, o2 -> if (o1.timestamp > o2.timestamp) -1 else 1 }
adapter.set(itt)
}
}

View File

@ -4,27 +4,48 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<group android:scaleX="0.15685186"
android:scaleY="0.15685186"
android:translateX="28.59"
android:translateY="28.59">
<path
android:pathData="M200,235h11v49h-11z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:startY="259.5"
android:startX="200"
android:endY="259.5"
android:endX="211"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
<item android:offset="0" android:color="#7F80E27E"/>
<item android:offset="1" android:color="#0080E27E"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
android:pathData="M284,162H40M162,40V284"
android:strokeAlpha="0.5"
android:strokeLineJoin="round"
android:strokeWidth="76"
android:fillColor="#00000000"
android:strokeColor="#80E27E"
android:strokeLineCap="round"/>
<path
android:pathData="M199.975,232V212.975C199.975,205.795 205.795,199.975 212.975,199.975H290C302.15,199.975 312,190.125 312,177.975V146.025C312,133.875 302.15,124.025 290,124.025H212.975C205.795,124.025 199.975,118.205 199.975,111.025V34C199.975,21.85 190.125,12 177.975,12H146.025C133.875,12 124.025,21.85 124.025,34V111.025C124.025,118.205 118.205,124.025 111.025,124.025H34C21.85,124.025 12,133.875 12,146.025V177.975C12,190.125 21.85,199.975 34,199.975H111.025C118.205,199.975 124.025,205.795 124.025,212.975V290C124.025,302.15 133.875,312 146.025,312H177.975C190.125,312 199.975,302.15 199.975,290V286.5"
android:strokeWidth="23"
android:fillColor="#00000000"
android:strokeLineCap="round">
<aapt:attr name="android:strokeColor">
<gradient
android:startY="12"
android:startX="12"
android:endY="312"
android:endX="312"
android:type="linear">
<item android:offset="0" android:color="#FF80E27E"/>
<item android:offset="1" android:color="#FF4CAF50"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,46 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="324dp"
android:height="324dp"
android:viewportWidth="324"
android:viewportHeight="324">
<path
android:pathData="M200,235h11v49h-11z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="259.5"
android:startX="200"
android:endY="259.5"
android:endX="211"
android:type="linear">
<item android:offset="0" android:color="#7F80E27E"/>
<item android:offset="1" android:color="#0080E27E"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M284,162H40M162,40V284"
android:strokeAlpha="0.5"
android:strokeLineJoin="round"
android:strokeWidth="76"
android:fillColor="#00000000"
android:strokeColor="#80E27E"
android:strokeLineCap="round"/>
<path
android:pathData="M199.975,232V212.975C199.975,205.795 205.795,199.975 212.975,199.975H290C302.15,199.975 312,190.125 312,177.975V146.025C312,133.875 302.15,124.025 290,124.025H212.975C205.795,124.025 199.975,118.205 199.975,111.025V34C199.975,21.85 190.125,12 177.975,12H146.025C133.875,12 124.025,21.85 124.025,34V111.025C124.025,118.205 118.205,124.025 111.025,124.025H34C21.85,124.025 12,133.875 12,146.025V177.975C12,190.125 21.85,199.975 34,199.975H111.025C118.205,199.975 124.025,205.795 124.025,212.975V290C124.025,302.15 133.875,312 146.025,312H177.975C190.125,312 199.975,302.15 199.975,290V286.5"
android:strokeWidth="23"
android:fillColor="#00000000"
android:strokeLineCap="round">
<aapt:attr name="android:strokeColor">
<gradient
android:startY="12"
android:startX="12"
android:endY="312"
android:endX="312"
android:type="linear">
<item android:offset="0" android:color="#FF80E27E"/>
<item android:offset="1" android:color="#FF4CAF50"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="324dp"
android:height="324dp"
android:viewportWidth="324"
android:viewportHeight="324">
<path
android:pathData="M200,235h11v49h-11z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="259.5"
android:startX="200"
android:endY="259.5"
android:endX="211"
android:type="linear">
<item android:offset="0" android:color="#7FFFFFFF"/>
<item android:offset="1" android:color="#00FFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M284,162H40M162,40V284"
android:strokeAlpha="0.5"
android:strokeLineJoin="round"
android:strokeWidth="76"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M199.975,232V212.975C199.975,205.795 205.795,199.975 212.975,199.975H290C302.15,199.975 312,190.125 312,177.975V146.025C312,133.875 302.15,124.025 290,124.025H212.975C205.795,124.025 199.975,118.205 199.975,111.025V34C199.975,21.85 190.125,12 177.975,12H146.025C133.875,12 124.025,21.85 124.025,34V111.025C124.025,118.205 118.205,124.025 111.025,124.025H34C21.85,124.025 12,133.875 12,146.025V177.975C12,190.125 21.85,199.975 34,199.975H111.025C118.205,199.975 124.025,205.795 124.025,212.975V290C124.025,302.15 133.875,312 146.025,312H177.975C190.125,312 199.975,302.15 199.975,290V286.5"
android:strokeWidth="23"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M0,0h512v512h-512z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="0"
android:startX="0"
android:endY="512"
android:endX="512"
android:type="linear">
<item android:offset="0" android:color="#FF80E27E"/>
<item android:offset="1" android:color="#FF4CAF50"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -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">
<TextView
android:id="@+id/text_slideshow"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -28,7 +28,7 @@
<fragment
android:id="@+id/nav_import"
android:name="com.dzeio.openhealth.ui.main.import.ImportFragment"
android:name="com.dzeio.openhealth.ui.main.imports.ImportFragment"
android:label="@string/menu_import"
tools:layout="@layout/fragment_import" />

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>