Finally have GoogleFit YaY
17
.idea/deploymentTargetDropDown.xml
generated
@ -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
@ -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" />
|
||||
|
@ -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"
|
||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -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?) {
|
||||
|
@ -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)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.dzeio.openhealth.connectors
|
||||
|
||||
enum class DataType {
|
||||
WEIGHT
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
} 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.")
|
||||
Log.d(TAG, "Connected!")
|
||||
if (isPermissionAcquired()) {
|
||||
reporter.start()
|
||||
} else {
|
||||
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))
|
||||
private val store : HealthDataStore = HealthDataStore(context, listener)
|
||||
|
||||
_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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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.
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
<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: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: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" />
|
||||
</vector>
|
||||
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>
|
||||
|
@ -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>
|
46
app/src/main/res/drawable/ic_logo_app.xml
Normal 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>
|
35
app/src/main/res/drawable/ic_logo_app_white.xml
Normal 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>
|
21
app/src/main/res/drawable/ic_logo_background.xml
Normal 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>
|
@ -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"
|
||||
|
@ -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" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@ -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" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 7.6 KiB |
@ -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" />
|
||||
|
||||
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|