1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-24 03:42:13 +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/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/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/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/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/app_bar_main.xml" value="0.5192107995846313" />
<entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/content_main.xml" value="0.5135869565217391" /> <entry key="..\:/git/Dzeio/OpenHealth/app/src/main/res/layout/content_main.xml" value="0.5135869565217391" />

View File

@ -23,7 +23,7 @@
<!-- Samsung Health--> <!-- Samsung Health-->
<meta-data <meta-data
android:name="com.samsung.android.health.permission.read" android:name="com.samsung.android.health.permission.read"
android:value="com.samsung.health.weight" /> android:value="com.samsung.health.step_count" />
<meta-data <meta-data
android:name="com.google.android.gms.version" 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) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Log.d("MainActivity", "Result $requestCode") 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?) { 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.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import com.dzeio.openhealth.data.AppDatabase
import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.data.weight.Weight
import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.fitness.Fitness import com.google.android.gms.fitness.Fitness
import com.google.android.gms.fitness.FitnessOptions import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.data.DataPoint import com.google.android.gms.fitness.data.DataPoint
import com.google.android.gms.fitness.data.DataSet 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.data.DataType
import com.google.android.gms.fitness.request.DataReadRequest 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.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.* import java.time.*
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
enum class ActionRequestCode {
FIND_DATA_SOURCES
}
class GoogleFit( class GoogleFit(
private val activity: Activity, private val activity: Activity,
) { ) : ConnectorInterface {
companion object { companion object {
const val TAG = "GoogleFitConnector" const val TAG = "GoogleFitConnector"
} }
// private val fitnessOptions = FitnessOptions.builder()
// .addDataType(DataType.TYPE_ACTIVITY_SEGMENT, FitnessOptions.ACCESS_READ) override val sourceID: String = "GoogleFit"
// .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() 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) .addDataType(DataType.TYPE_WEIGHT)
.build() .build()
private val runningQOrLater = private val runningQOrLater =
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q 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() { fun import() {
checkPermissionsAndRun(ActionRequestCode.FIND_DATA_SOURCES) checkPermissionsAndRun()
} }
private fun checkPermissionsAndRun(actionRequestCode: ActionRequestCode) { private fun checkPermissionsAndRun() {
if (permissionApproved()) { if (permissionApproved()) {
signIn(actionRequestCode) signIn(0)
} else { } else {
requestRuntimePermissions(actionRequestCode) requestRuntimePermissions()
}
}
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)
}
} }
} }
@ -118,15 +61,41 @@ class GoogleFit(
return approved return approved
} }
fun signIn(requestCode: ActionRequestCode) { private fun requestRuntimePermissions() {
if (oAuthPermissionsApproved()) { val shouldProvideRationale =
performActionForRequestCode(requestCode) 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 { } 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 { requestCode.let {
GoogleSignIn.requestPermissions( GoogleSignIn.requestPermissions(
activity, activity,
it.ordinal, it,
getGoogleAccount(), fitnessOptions) getGoogleAccount(), fitnessOptions)
} }
} }
} }
@ -141,103 +110,7 @@ class GoogleFit(
*/ */
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions) private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
/** private fun internalImportWeight() {
* 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 calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val now = Date() val now = Date()
@ -257,7 +130,7 @@ class GoogleFit(
// In this example, it's very unlikely that the request is for several hundred // 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 // 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. // 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) .read(DataType.TYPE_WEIGHT)
// Analogous to a "Group By" in SQL, defines how data should be aggregated. // Analogous to a "Group By" in SQL, defines how data should be aggregated.
@ -267,110 +140,68 @@ class GoogleFit(
// .bucketByActivityType(1, TimeUnit.SECONDS) // .bucketByActivityType(1, TimeUnit.SECONDS)
// .bucketBySession() // .bucketBySession()
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS) .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)) Fitness.getHistoryClient(activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions))
.readData(request) .readData(request)
.addOnSuccessListener { response -> .addOnSuccessListener { response ->
// The aggregate query puts datasets into buckets, so flatten into a // The aggregate query puts datasets into buckets, so flatten into a
// single list of datasets // 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) { for (dataSet in response.dataSets) {
dumpDataSet(dataSet) dumpDataSet(dataSet)
} }
for (dataSet in response.buckets.flatMap { it.dataSets }) { // for (dataSet in response.buckets.flatMap { it.dataSets }) {
dumpDataSet(dataSet) // dumpDataSet(dataSet)
} // }
callback.invoke()
} }
.addOnFailureListener { e -> .addOnFailureListener { e ->
Log.w(TAG,"There was an error reading data from Google Fit", e) Log.w(TAG,"There was an error reading data from Google Fit", e)
callback.invoke()
} }
} }
private fun dumpDataSet(dataSet: DataSet) { private fun dumpDataSet(dataSet: DataSet) {
Log.i(TAG, "Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size}") 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() val weight = Weight()
Log.i(TAG,"Data point:") Log.i(TAG,"Data point:")
Log.i(TAG,"\tType: ${dp.dataType.name}") Log.i(TAG,"\tType: ${dp.dataType.name}")
Log.i(TAG,"\tStart: ${dp.getStartTimeString()}") Log.i(TAG,"\tStart: ${dp.getStartTimeString()}")
Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}") Log.i(TAG,"\tEnd: ${dp.getEndTimeString()}")
weight.timestamp = dp.getStartTime(TimeUnit.SECONDS) weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS)
weight.source = "GoogleFit" weight.source = "GoogleFit"
for (field in dp.dataType.fields) { for (field in dp.dataType.fields) {
weight.weight = dp.getValue(field).asFloat() weight.weight = dp.getValue(field).asFloat()
Log.i(TAG,"\tField: ${field.name.toString()} Value: ${dp.getValue(field)}") 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 = fun DataPoint.getStartTimeString(): String = Date(this.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString()
SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
.format(Date(this.getStartTime(TimeUnit.SECONDS) * 1000L))
fun DataPoint.getEndTimeString(): String = fun DataPoint.getEndTimeString(): String = Date(this.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString()
SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS", Locale.FRANCE)
.format(Date(this.getEndTime(TimeUnit.SECONDS) * 1000L))
override fun onRequestPermissionResult(
/** Unregisters the listener with the Sensors API. */ requestCode: Int,
private fun unregisterFitnessDataListener() { permission: Array<String>,
if (dataPointListener == null) { grantResult: IntArray
// This code only activates one listener at a time. If there's no listener, there's ) {
// nothing to unregister. signIn(requestCode)
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. */ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
private fun queryFitnessData(): DataReadRequest { signIn(requestCode)
// [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() private lateinit var callback: (weight: Weight, end: Boolean) -> Unit
Log.i(TAG, "Range Start: ${dateFormat.format(startTime)}")
Log.i(TAG, "Range End: ${dateFormat.format(endTime)}")
return DataReadRequest.Builder() override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) {
// The data request can specify multiple data types to return, effectively this.callback = callback
// combining multiple data queries into one call. checkPermissionsAndRun()
// 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

@ -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 package com.dzeio.openhealth.connectors.samsunghealth
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.content.Intent
import android.content.DialogInterface import android.os.Handler
import android.os.Looper
import android.util.Log 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.*
import com.samsung.android.sdk.healthdata.HealthConstants.StepCount
import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener
import com.samsung.android.sdk.healthdata.HealthPermissionManager.* import com.samsung.android.sdk.healthdata.HealthPermissionManager.*
import com.samsung.android.sdk.healthdata.HealthResultHolder.ResultListener import java.util.*
import java.lang.Boolean
import kotlin.Exception
class SamsungHealth( class SamsungHealth(
private val context: Activity private val context: Activity
) { ) : ConnectorInterface {
companion object { companion object {
const val TAG = "SamsungHealthConnector" 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> = private val listener = object : ConnectionListener {
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 {
override fun onConnected() { override fun onConnected() {
Log.d(TAG, "Health data service is connected.") Log.d(TAG, "Connected!")
val pmsManager = HealthPermissionManager(store) if (isPermissionAcquired()) {
try { reporter.start()
val resultMap = pmsManager.isPermissionAcquired(keySet)
if (resultMap.containsValue(Boolean.FALSE)) {
// Request the permission for reading step counts if it is not acquired
pmsManager.requestPermissions(keySet, context)
.setResultListener(permissionListener)
} else { } else {
// Get the current step count and display it requestPermission()
getStepCount()
}
} catch (e: Exception) {
Log.e(TAG, e.javaClass.name + " - " + e.message)
Log.e(TAG, "Permission setting fails.")
} }
} }
override fun onConnectionFailed(p0: HealthConnectionErrorResult?) {
override fun onConnectionFailed(error: HealthConnectionErrorResult) {
Log.d(TAG, "Health data service is not available.") Log.d(TAG, "Health data service is not available.")
showConnectionFailureDialog(error)
} }
override fun onDisconnected() { override fun onDisconnected() {
Log.d(TAG, "Health data service is disconnected.") Log.d(TAG, "Health data service is disconnected.")
} }
} }
init { private val store : HealthDataStore = HealthDataStore(context, listener)
//keySet = HashSet()
keySet.add(PermissionKey(HealthConstants.Weight.HEALTH_DATA_TYPE, PermissionType.READ))
_store = HealthDataStore(context, connectionListener) private fun isPermissionAcquired(): Boolean {
val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ)
val pmsManager = HealthPermissionManager(store)
try {
// Check whether the permissions that this application needs are acquired
val resultMap = pmsManager.isPermissionAcquired(setOf(permKey))
return !resultMap.containsValue(java.lang.Boolean.FALSE)
} catch (e: java.lang.Exception) {
Log.e(TAG, "Permission request fails.", e)
}
return false
} }
fun test() { private fun requestPermission() {
val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ)
val pmsManager = HealthPermissionManager(store)
try {
// Show user permission UI for allowing user to change options
pmsManager.requestPermissions(setOf(permKey), context)
.setResultListener { result: PermissionResult ->
Log.d(TAG, "Permission callback is received.")
val resultMap =
result.resultMap
if (resultMap.containsValue(java.lang.Boolean.FALSE)) {
Log.d(TAG, "No Data???")
} else {
// Get the current step count and display it
reporter.start()
}
}
} catch (e: Exception) {
Log.e(TAG, "Permission setting fails.", e)
}
}
private val stepCountObserver = object : StepCountReporter.StepCountObserver {
override fun onChanged(count: Int) {
Log.d(TAG, "Step reported : $count")
}
}
private val reporter = StepCountReporter(store, stepCountObserver, Handler(Looper.getMainLooper()))
/**
* Connector
*/
override val sourceID: String = "SamsungHealth"
override fun onRequestPermissionResult(
requestCode: Int,
permission: Array<String>,
grantResult: IntArray
) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
override fun importWeight(callback: (weight: Weight, end: Boolean) -> Unit) {
store.connectService() store.connectService()
} }
private fun getStepCount() {
val resolver = HealthDataResolver(store, null)
val request = HealthDataResolver.ReadRequest.Builder()
.setDataType(HealthConstants.Weight.HEALTH_DATA_TYPE)
.setLocalTimeRange(HealthConstants.Weight.START_TIME, HealthConstants.Weight.TIME_OFFSET, 0, System.currentTimeMillis())
.build()
try {
val res = resolver.read(request).await()
res.use { res1 ->
val iterator = res1.iterator()
if (iterator.hasNext()) {
val data = iterator.next()
val value = data.getFloat(HealthConstants.Weight.WEIGHT)
val time = data.getLong(HealthConstants.Weight.CREATE_TIME)
Log.d(TAG, "Data received! $value $time")
}
}
} catch (e : Exception) {
Log.d(TAG, "Reading failed!")
}
}
private fun showConnectionFailureDialog(error: HealthConnectionErrorResult) {
val alert: AlertDialog.Builder = AlertDialog.Builder(context)
var message = "Connection with Samsung Health is not available"
if (error.hasResolution()) {
message = when (error.errorCode) {
HealthConnectionErrorResult.PLATFORM_NOT_INSTALLED -> "Please install Samsung Health"
HealthConnectionErrorResult.OLD_VERSION_PLATFORM -> "Please upgrade Samsung Health"
HealthConnectionErrorResult.PLATFORM_DISABLED -> "Please enable Samsung Health"
HealthConnectionErrorResult.USER_AGREEMENT_NEEDED -> "Please agree with Samsung Health policy"
else -> "Please make Samsung Health available"
}
}
alert.setMessage(message)
alert.setPositiveButton("OK", DialogInterface.OnClickListener { dialog, id ->
if (error.hasResolution()) {
error.resolve(context)
}
})
if (error.hasResolution()) {
alert.setNegativeButton("Cancel", null)
}
alert.show()
}
fun destroy() {
store.disconnectService()
}
} }

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)") @Query("Select * FROM Weight WHERE id=(SELECT max(id) FROM Weight)")
fun last(): Flow<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 addWeight(weight: Weight) = weightDao.insert(weight)
suspend fun deleteWeight(weight: Weight) = weightDao.delete(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.app.ProgressDialog
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -9,53 +10,38 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.ViewModelProvider import com.dzeio.openhealth.connectors.ConnectorInterface
import com.dzeio.openhealth.connectors.ActionRequestCode
import com.dzeio.openhealth.connectors.GoogleFit import com.dzeio.openhealth.connectors.GoogleFit
//import com.dzeio.openhealth.connectors.GoogleFit //import com.dzeio.openhealth.connectors.GoogleFit
import com.dzeio.openhealth.connectors.samsunghealth.SamsungHealth import com.dzeio.openhealth.connectors.samsunghealth.SamsungHealth
import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.databinding.FragmentImportBinding 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 { companion object {
const val TAG = "ImportFragment" const val TAG = "ImportFragment"
} }
private var _binding: FragmentImportBinding? = null override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentImportBinding = FragmentImportBinding::inflate
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var viewModel: ImportViewModel
private lateinit var progressDialog: ProgressDialog private lateinit var progressDialog: ProgressDialog
private lateinit var fit: GoogleFit private lateinit var fit: ConnectorInterface
override fun onCreateView( override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
inflater: LayoutInflater, super.onViewCreated(view, savedInstanceState)
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewModel = ViewModelProvider(this).get(ImportViewModel::class.java)
_binding = FragmentImportBinding.inflate(inflater, container, false)
val root: View = binding.root
progressDialog = ProgressDialog(requireContext()) progressDialog = ProgressDialog(requireContext())
progressDialog.apply { progressDialog.apply {
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
setCancelable(false) setCancelable(false)
setTitle("Importing from source...")
} }
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.importGoogleFit.setOnClickListener { binding.importGoogleFit.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
importFromGoogleFit() importFromGoogleFit()
@ -66,29 +52,67 @@ class ImportFragment : Fragment() {
} }
} }
override fun onDestroyView() { private fun importFromGoogleFit() {
super.onDestroyView() progressDialog.show()
_binding = null
}
@RequiresApi(Build.VERSION_CODES.O)
fun importFromGoogleFit() {
viewModel.importProgressTotal.postValue(-1)
fit = GoogleFit(requireActivity()) fit = GoogleFit(requireActivity())
progressDialog.show() var imported = 0
fit.import()
fit.getHistory() lifecycleScope.launchWhenStarted {
// google.importWeight { viewModel.deleteFromSource(fit.sourceID)
// progressDialog.dismiss() }.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() { fun importFromSamsungHealth() {
SamsungHealth(requireActivity()) progressDialog.show()
.test() 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>, override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) { grantResults: IntArray) {
when { when {
@ -100,11 +124,7 @@ class ImportFragment : Fragment() {
grantResults[0] == PackageManager.PERMISSION_GRANTED -> { grantResults[0] == PackageManager.PERMISSION_GRANTED -> {
Log.d(TAG, "Granted") Log.d(TAG, "Granted")
// Permission was granted. fit.onRequestPermissionResult(requestCode, permissions, grantResults)
val fitActionRequestCode = ActionRequestCode.values()[requestCode]
fitActionRequestCode.let {
fit.signIn(ActionRequestCode.FIND_DATA_SOURCES)
}
} }
else -> { else -> {
// Permission denied. // 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 androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.openhealth.adapters.WeightAdapter import com.dzeio.openhealth.adapters.WeightAdapter
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.data.weight.Weight
import com.dzeio.openhealth.databinding.FragmentListWeightBinding import com.dzeio.openhealth.databinding.FragmentListWeightBinding
import com.dzeio.openhealth.ui.dialogs.EditWeightDialog import com.dzeio.openhealth.ui.dialogs.EditWeightDialog
import com.dzeio.openhealth.ui.main.home.HomeViewModel import com.dzeio.openhealth.ui.main.home.HomeViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import java.util.*
@AndroidEntryPoint @AndroidEntryPoint
class ListWeightFragment : BaseFragment<HomeViewModel, FragmentListWeightBinding>(HomeViewModel::class.java) { class ListWeightFragment : BaseFragment<HomeViewModel, FragmentListWeightBinding>(HomeViewModel::class.java) {
@ -37,8 +40,10 @@ class ListWeightFragment : BaseFragment<HomeViewModel, FragmentListWeightBinding
recycler.adapter = adapter recycler.adapter = adapter
viewLifecycleOwner.lifecycleScope.launchWhenCreated { viewLifecycleOwner.lifecycleScope.launchWhenCreated {
viewModel.fetchWeights().collect { viewModel.fetchWeights().collectLatest {
adapter.set(it) 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:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="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"> <aapt:attr name="android:fillColor">
<gradient <gradient
android:endX="85.84757" android:startY="259.5"
android:endY="92.4963" android:startX="200"
android:startX="42.9492" android:endY="259.5"
android:startY="49.59793" android:endX="211"
android:type="linear"> android:type="linear">
<item <item android:offset="0" android:color="#7F80E27E"/>
android:color="#44000000" <item android:offset="1" android:color="#0080E27E"/>
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
<path <path
android:fillColor="#FFFFFF" android:pathData="M284,162H40M162,40V284"
android:fillType="nonZero" android:strokeAlpha="0.5"
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:strokeLineJoin="round"
android:strokeWidth="1" android:strokeWidth="76"
android:strokeColor="#00000000" /> 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> </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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.main.import.ImportFragment"> tools:context=".ui.main.imports.ImportFragment">
<TextView <TextView
android:id="@+id/text_slideshow" android:id="@+id/text_slideshow"

View File

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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <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" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </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 <fragment
android:id="@+id/nav_import" 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" android:label="@string/menu_import"
tools:layout="@layout/fragment_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>