mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-23 03:12:14 +00:00
feat: Add base for Food monitoring
This commit is contained in:
parent
e2b4ece9e4
commit
9d3585c42c
@ -28,8 +28,6 @@ Permissions requests are for specifics usage and are only requests the first tim
|
|||||||
|
|
||||||
| Permission | Why is it requested |
|
| Permission | Why is it requested |
|
||||||
|:----------------------:|:-----------------------------------------------------------------|
|
|:----------------------:|:-----------------------------------------------------------------|
|
||||||
| ACCESS_FINE_LOCATION | Google Fit Extension Requirement (maybe not, still have to test) |
|
|
||||||
| ACCESS_COARSE_LOCATION | Same as above |
|
|
||||||
| ACTIVITY_RECOGNITION | Device Steps Usage |
|
| ACTIVITY_RECOGNITION | Device Steps Usage |
|
||||||
|
|
||||||
No other permissions are used (even the internet permission ;)).
|
No other permissions are used (even the internet permission ;)).
|
||||||
|
@ -129,7 +129,7 @@ dependencies {
|
|||||||
implementation("androidx.core:core-ktx:1.9.0")
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0-alpha01")
|
implementation("androidx.appcompat:appcompat:1.7.0-alpha01")
|
||||||
implementation("javax.inject:javax.inject:1")
|
implementation("javax.inject:javax.inject:1")
|
||||||
implementation("com.google.android.material:material:1.8.0-alpha02")
|
implementation("com.google.android.material:material:1.8.0-alpha03")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
||||||
@ -174,7 +174,7 @@ dependencies {
|
|||||||
|
|
||||||
// Google Fit
|
// Google Fit
|
||||||
implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
||||||
implementation("com.google.android.gms:play-services-auth:20.3.0")
|
implementation("com.google.android.gms:play-services-auth:20.4.0")
|
||||||
implementation("androidx.health.connect:connect-client:1.0.0-alpha07")
|
implementation("androidx.health.connect:connect-client:1.0.0-alpha07")
|
||||||
|
|
||||||
// Samsung Health
|
// Samsung Health
|
||||||
@ -194,4 +194,8 @@ dependencies {
|
|||||||
|
|
||||||
// OSS Licenses
|
// OSS Licenses
|
||||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||||
|
|
||||||
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
|
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||||
|
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
|
||||||
}
|
}
|
||||||
|
220
app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json
Normal file
220
app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "794ed5ee15db239f9a2708b951f55552",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Weight",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `weight` REAL NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "weight",
|
||||||
|
"columnName": "weight",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Weight_timestamp",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Weight_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Water",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Water_timestamp",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Water_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Step",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Step_timestamp",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Step_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Food",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `quantity` INTEGER NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `timestamp` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "quantity",
|
||||||
|
"columnName": "quantity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "proteins",
|
||||||
|
"columnName": "proteins",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "carbohydrates",
|
||||||
|
"columnName": "carbohydrates",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "fat",
|
||||||
|
"columnName": "fat",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "energy",
|
||||||
|
"columnName": "energy",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '794ed5ee15db239f9a2708b951f55552')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package com.dzeio.openhealth
|
package com.dzeio.openhealth
|
||||||
|
|
||||||
import com.dzeio.openhealth.extensions.Extension
|
|
||||||
|
|
||||||
object Settings {
|
object Settings {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,9 +26,4 @@ object Settings {
|
|||||||
* format in which the weight the user want it to be displayed as
|
* format in which the weight the user want it to be displayed as
|
||||||
*/
|
*/
|
||||||
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
|
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
|
||||||
|
|
||||||
fun extensionEnabled(extension: Extension): String {
|
|
||||||
return "com.dzeio.open-health.extension.${extension.id}.enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
package com.dzeio.openhealth.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.dzeio.openhealth.Settings
|
|
||||||
import com.dzeio.openhealth.core.BaseAdapter
|
|
||||||
import com.dzeio.openhealth.core.BaseViewHolder
|
|
||||||
import com.dzeio.openhealth.databinding.LayoutExtensionItemBinding
|
|
||||||
import com.dzeio.openhealth.extensions.Extension
|
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionAdapter(
|
|
||||||
private val config: Configuration
|
|
||||||
) : BaseAdapter<Extension, LayoutExtensionItemBinding>() {
|
|
||||||
|
|
||||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) ->
|
|
||||||
LayoutExtensionItemBinding = LayoutExtensionItemBinding::inflate
|
|
||||||
|
|
||||||
var onItemClick: ((weight: Extension) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onBindData(
|
|
||||||
holder: BaseViewHolder<LayoutExtensionItemBinding>,
|
|
||||||
item: Extension,
|
|
||||||
position: Int
|
|
||||||
) {
|
|
||||||
val isEnabled = config.getBoolean(Settings.extensionEnabled(item)).value ?: false
|
|
||||||
holder.binding.name.text = item.name
|
|
||||||
holder.binding.card.isClickable = item.isAvailable()
|
|
||||||
holder.binding.card.isEnabled = item.isAvailable()
|
|
||||||
holder.binding.status.text = "enabled = $isEnabled"
|
|
||||||
if (item.isAvailable()) {
|
|
||||||
holder.binding.card.setOnClickListener {
|
|
||||||
onItemClick?.invoke(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.dzeio.openhealth.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.dzeio.openhealth.core.BaseAdapter
|
||||||
|
import com.dzeio.openhealth.core.BaseViewHolder
|
||||||
|
import com.dzeio.openhealth.data.food.Food
|
||||||
|
import com.dzeio.openhealth.databinding.ItemFoodBinding
|
||||||
|
|
||||||
|
class FoodAdapter() : BaseAdapter<Food, ItemFoodBinding>() {
|
||||||
|
|
||||||
|
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ItemFoodBinding
|
||||||
|
get() = ItemFoodBinding::inflate
|
||||||
|
|
||||||
|
var onItemClick: ((weight: Food) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onBindData(
|
||||||
|
holder: BaseViewHolder<ItemFoodBinding>,
|
||||||
|
item: Food,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
holder.binding.foodName.text = item.name
|
||||||
|
holder.binding.foodDescription.text = item.energy.toString()
|
||||||
|
holder.binding.edit.setOnClickListener {
|
||||||
|
onItemClick?.invoke(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
package com.dzeio.openhealth.data
|
package com.dzeio.openhealth.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import com.dzeio.openhealth.data.food.Food
|
||||||
|
import com.dzeio.openhealth.data.food.FoodDao
|
||||||
import com.dzeio.openhealth.data.step.Step
|
import com.dzeio.openhealth.data.step.Step
|
||||||
import com.dzeio.openhealth.data.step.StepDao
|
import com.dzeio.openhealth.data.step.StepDao
|
||||||
import com.dzeio.openhealth.data.water.Water
|
import com.dzeio.openhealth.data.water.Water
|
||||||
@ -15,10 +18,14 @@ import com.dzeio.openhealth.data.weight.WeightDao
|
|||||||
entities = [
|
entities = [
|
||||||
Weight::class,
|
Weight::class,
|
||||||
Water::class,
|
Water::class,
|
||||||
Step::class
|
Step::class,
|
||||||
|
Food::class
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 2,
|
||||||
exportSchema = true
|
exportSchema = true,
|
||||||
|
autoMigrations = [
|
||||||
|
AutoMigration(from = 1, to = 2)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
@ -28,6 +35,8 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun waterDao(): WaterDao
|
abstract fun waterDao(): WaterDao
|
||||||
abstract fun stepDao(): StepDao
|
abstract fun stepDao(): StepDao
|
||||||
|
|
||||||
|
abstract fun foodDao(): FoodDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DATABASE_NAME = "open_health"
|
private const val DATABASE_NAME = "open_health"
|
||||||
|
|
||||||
|
20
app/src/main/java/com/dzeio/openhealth/data/food/Food.kt
Normal file
20
app/src/main/java/com/dzeio/openhealth/data/food/Food.kt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package com.dzeio.openhealth.data.food
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Food(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var id: Long = 0,
|
||||||
|
var name: String,
|
||||||
|
var quantity: Int,
|
||||||
|
var proteins: Float,
|
||||||
|
var carbohydrates: Float,
|
||||||
|
var fat: Float,
|
||||||
|
var energy: Float,
|
||||||
|
|
||||||
|
var timestamp: Long = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis
|
||||||
|
)
|
22
app/src/main/java/com/dzeio/openhealth/data/food/FoodDao.kt
Normal file
22
app/src/main/java/com/dzeio/openhealth/data/food/FoodDao.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package com.dzeio.openhealth.data.food
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.dzeio.openhealth.core.BaseDao
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface FoodDao : BaseDao<Food> {
|
||||||
|
@Query("SELECT * FROM Food ORDER BY timestamp DESC")
|
||||||
|
fun getAll(): Flow<List<Food>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Food where id = :weightId")
|
||||||
|
fun getOne(weightId: Long): Flow<Food?>
|
||||||
|
|
||||||
|
@Query("Select count(*) from Food")
|
||||||
|
fun getCount(): Flow<Int>
|
||||||
|
|
||||||
|
@Query("Select * FROM Food ORDER BY timestamp DESC LIMIT 1")
|
||||||
|
fun last(): Flow<Food?>
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.dzeio.openhealth.data.food
|
||||||
|
|
||||||
|
import com.dzeio.openhealth.data.openfoodfact.OFFResult
|
||||||
|
import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService
|
||||||
|
import retrofit2.Response
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class FoodRepository @Inject constructor(
|
||||||
|
private val dao: FoodDao,
|
||||||
|
private val offSource: OpenFoodFactService
|
||||||
|
) {
|
||||||
|
suspend fun findOnlineFood(name: String): Response<OFFResult> {
|
||||||
|
return offSource.searchProducts(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAll() = dao.getAll()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.dzeio.openhealth.data.openfoodfact
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class OFFNutriments(
|
||||||
|
@SerializedName("carbohydrates_100g")
|
||||||
|
var carbohydrates: Float,
|
||||||
|
@SerializedName("energy-kcal_100g")
|
||||||
|
var energy: Float,
|
||||||
|
@SerializedName("fat_100g")
|
||||||
|
var fat: Float,
|
||||||
|
@SerializedName("proteins_100g")
|
||||||
|
var proteins: Float
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.dzeio.openhealth.data.openfoodfact
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class OFFProduct(
|
||||||
|
@SerializedName("_id")
|
||||||
|
var id: String,
|
||||||
|
@SerializedName("product_name")
|
||||||
|
var name: String,
|
||||||
|
|
||||||
|
@SerializedName("serving_quantity")
|
||||||
|
var serving: String,
|
||||||
|
|
||||||
|
@SerializedName("nutriments")
|
||||||
|
var nutriments: OFFNutriments
|
||||||
|
)
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.dzeio.openhealth.data.openfoodfact
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class OFFResult(
|
||||||
|
@SerializedName("products")
|
||||||
|
var products: List<OFFProduct>
|
||||||
|
)
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.dzeio.openhealth.data.openfoodfact
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface OpenFoodFactService {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getService(): OpenFoodFactService {
|
||||||
|
val interceptor = HttpLoggingInterceptor()
|
||||||
|
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.build()
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.setLenient()
|
||||||
|
.create()
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://world.openfoodfacts.org/")
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return retrofit.create(OpenFoodFactService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Headers("User-Agent: OpenHealth - Android - Version 1.0 - https://github.com/dzeiocom/OpenHealth")
|
||||||
|
@GET("/api/v2/search?fields=_id,nutriments,product_name,serving_quantity")
|
||||||
|
suspend fun searchProducts(@Query("product_name") name: String): Response<OFFResult>
|
||||||
|
}
|
@ -2,6 +2,8 @@ package com.dzeio.openhealth.di
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.dzeio.openhealth.data.AppDatabase
|
import com.dzeio.openhealth.data.AppDatabase
|
||||||
|
import com.dzeio.openhealth.data.food.FoodDao
|
||||||
|
import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService
|
||||||
import com.dzeio.openhealth.data.step.StepDao
|
import com.dzeio.openhealth.data.step.StepDao
|
||||||
import com.dzeio.openhealth.data.water.WaterDao
|
import com.dzeio.openhealth.data.water.WaterDao
|
||||||
import com.dzeio.openhealth.data.weight.WeightDao
|
import com.dzeio.openhealth.data.weight.WeightDao
|
||||||
@ -36,4 +38,15 @@ class DatabaseModule {
|
|||||||
fun provideStepsDao(appDatabase: AppDatabase): StepDao {
|
fun provideStepsDao(appDatabase: AppDatabase): StepDao {
|
||||||
return appDatabase.stepDao()
|
return appDatabase.stepDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideFoodDao(appDatabase: AppDatabase): FoodDao {
|
||||||
|
return appDatabase.foodDao()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideOpenFoodFactService(): OpenFoodFactService {
|
||||||
|
return OpenFoodFactService.getService()
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,143 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension Schema
|
|
||||||
*
|
|
||||||
* Version: 0.2.0
|
|
||||||
*/
|
|
||||||
interface Extension : ActivityResultCallback<Any> {
|
|
||||||
|
|
||||||
data class TaskProgress<T>(
|
|
||||||
/**
|
|
||||||
* value indicating the current status of the task
|
|
||||||
*/
|
|
||||||
val state: TaskState = TaskState.INITIALIZATING,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* value between 0 and 100 indicating the progress for the task
|
|
||||||
*/
|
|
||||||
val progress: Float? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additionnal message that will be displayed when the task has ended in a [TaskState.CANCELLED] or [TaskState.ERROR] state
|
|
||||||
*/
|
|
||||||
val statusMessage: String? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional information
|
|
||||||
*/
|
|
||||||
val additionalData: T? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class TaskState {
|
|
||||||
/**
|
|
||||||
* define the task as being preped
|
|
||||||
*/
|
|
||||||
INITIALIZATING,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the task a bein worked on
|
|
||||||
*/
|
|
||||||
WORK_IN_PROGRESS,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* define the task as being done
|
|
||||||
*/
|
|
||||||
DONE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the task as being cancelled
|
|
||||||
*/
|
|
||||||
CANCELLED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* define the task as being ended with an error
|
|
||||||
*/
|
|
||||||
ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Data {
|
|
||||||
/**
|
|
||||||
* Special case to handle basic errors from other activities
|
|
||||||
*/
|
|
||||||
NOTHING,
|
|
||||||
WEIGHT,
|
|
||||||
STEPS
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Fit:
|
|
||||||
*
|
|
||||||
* STEP_COUNT_CUMULATIVE
|
|
||||||
* ACTIVITY_SEGMENT
|
|
||||||
* SLEEP_SEGMENT
|
|
||||||
* CALORIES_EXPENDED
|
|
||||||
* BASAL_METABOLIC_RATE
|
|
||||||
* POWER_SAMPLE
|
|
||||||
* HEART_RATE_BPM
|
|
||||||
* LOCATION_SAMPLE
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the permissions necessary for the extension to works
|
|
||||||
*/
|
|
||||||
val permissions: Array<String>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the Source ID
|
|
||||||
*
|
|
||||||
* DO NOT CHANGE IT AFTER THE EXTENSION IS IN PRODUCTION
|
|
||||||
*/
|
|
||||||
val id: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Extension Display Name
|
|
||||||
*/
|
|
||||||
val name: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the different types of data handled by the extension
|
|
||||||
*/
|
|
||||||
val data: Array<Data>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable the extension, no code is gonna be run before
|
|
||||||
*/
|
|
||||||
fun enable(fragment: Fragment): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return if the extension is already connected to remote of not
|
|
||||||
*/
|
|
||||||
suspend fun isConnected(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return if the extension is runnable on the device
|
|
||||||
*/
|
|
||||||
fun isAvailable(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* try to connect to remote
|
|
||||||
*/
|
|
||||||
suspend fun connect(): Boolean
|
|
||||||
|
|
||||||
val contract: ActivityResultContract<*, *>?
|
|
||||||
val requestInput: Any?
|
|
||||||
suspend fun importWeight(): Flow<TaskProgress<ArrayList<Weight>>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function run when outgoing sync is enabled and new value is added
|
|
||||||
* or manual export is launched
|
|
||||||
*/
|
|
||||||
suspend fun exportWeights(weight: Array<Weight>): Flow<TaskProgress<Unit>>
|
|
||||||
|
|
||||||
// fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = Unit
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun permissionsGranted(): Boolean
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
class ExtensionFactory {
|
|
||||||
companion object {
|
|
||||||
fun getExtension(extension: String): Extension? {
|
|
||||||
return when (extension) {
|
|
||||||
"GoogleFit" -> {
|
|
||||||
GoogleFitExtension()
|
|
||||||
}
|
|
||||||
"HealthConnect" -> {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
HealthConnectExtension()
|
|
||||||
} else {
|
|
||||||
TODO("VERSION.SDK_INT < P")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAll(): ArrayList<Extension> {
|
|
||||||
val extensions: ArrayList<Extension> = arrayListOf(
|
|
||||||
GoogleFitExtension()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
extensions.add(HealthConnectExtension())
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
|
||||||
|
|
||||||
class FileSystemExtension : Extension {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "FSExtension"
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var activity: Activity
|
|
||||||
|
|
||||||
override val id = "FileSystem"
|
|
||||||
override val name = "File System"
|
|
||||||
|
|
||||||
override val permissions = arrayOf(
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
|
|
||||||
override val permissionsText: String = "Please"
|
|
||||||
|
|
||||||
override fun init(activity: Activity): Array<Extension.Data> {
|
|
||||||
this.activity = activity
|
|
||||||
return Extension.Data.values()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(): String {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isConnected(): Boolean = true
|
|
||||||
|
|
||||||
private val connectLiveData: MutableLiveData<Extension.States> = MutableLiveData(Extension.States.DONE)
|
|
||||||
|
|
||||||
override fun connect(): LiveData<Extension.States> = connectLiveData
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
Log.d(this.name, "[$requestCode] -> [$resultCode]: $data")
|
|
||||||
if (requestCode == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultCode == Activity.RESULT_OK) connectLiveData.value = Extension.States.DONE
|
|
||||||
// signIn(Data.values()[requestCode])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun importWeight(): LiveData<Extension.ImportState<Weight>> {
|
|
||||||
|
|
||||||
weightLiveData = MutableLiveData(
|
|
||||||
Extension.ImportState(
|
|
||||||
Extension.States.WIP
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
startImport(Extension.Data.WEIGHT)
|
|
||||||
|
|
||||||
return weightLiveData
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,210 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
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.DataType
|
|
||||||
import com.google.android.gms.fitness.request.DataReadRequest
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.TimeZone
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class GoogleFit: Extension {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "GoogleFitConnector"
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var activity: Activity
|
|
||||||
|
|
||||||
override val id = "GoogleFit"
|
|
||||||
override val name = "Google Fit"
|
|
||||||
|
|
||||||
override val permissions = arrayOf(
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
|
|
||||||
override val permissionsText: String = "Please"
|
|
||||||
|
|
||||||
override fun init(activity: Fragment): Array<Extension.Data> {
|
|
||||||
this.activity = activity.register
|
|
||||||
return arrayOf(
|
|
||||||
Extension.Data.WEIGHT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatus(): String {
|
|
||||||
return if (isConnected()) "Connected" else "Not Connected"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isConnected(): Boolean =
|
|
||||||
GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions)
|
|
||||||
|
|
||||||
private val fitnessOptions = FitnessOptions.builder()
|
|
||||||
.addDataType(DataType.TYPE_WEIGHT)
|
|
||||||
// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE)
|
|
||||||
// .addDataType(DataType.TYPE_CALORIES_EXPENDED)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val connectLiveData: MutableLiveData<Extension.States> = MutableLiveData(Extension.States.WIP)
|
|
||||||
|
|
||||||
override fun connect(): LiveData<Extension.States> {
|
|
||||||
|
|
||||||
if (isConnected()) {
|
|
||||||
connectLiveData.value = Extension.States.DONE
|
|
||||||
} else {
|
|
||||||
Log.d(this.name, "Signing In")
|
|
||||||
GoogleSignIn.requestPermissions(
|
|
||||||
activity,
|
|
||||||
124887,
|
|
||||||
getGoogleAccount(), fitnessOptions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return connectLiveData
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
|
|
||||||
|
|
||||||
private val timeRange by lazy {
|
|
||||||
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
|
||||||
calendar.time = Date()
|
|
||||||
val endTime = calendar.timeInMillis
|
|
||||||
|
|
||||||
// Set year to 2013 to be sure to get data from when Google Fit Started to today
|
|
||||||
calendar.set(Calendar.YEAR, 2013)
|
|
||||||
val startTime = calendar.timeInMillis
|
|
||||||
return@lazy arrayOf(startTime, endTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startImport(data: Extension.Data) {
|
|
||||||
Log.d("GoogleFitImporter", "Importing for ${data.name}")
|
|
||||||
|
|
||||||
val dateFormat = DateFormat.getDateInstance()
|
|
||||||
Log.i(TAG, "Range Start: ${dateFormat.format(timeRange[0])}")
|
|
||||||
Log.i(TAG, "Range End: ${dateFormat.format(timeRange[1])}")
|
|
||||||
|
|
||||||
var type = DataType.TYPE_WEIGHT
|
|
||||||
var timeUnit = TimeUnit.MILLISECONDS
|
|
||||||
|
|
||||||
when (data) {
|
|
||||||
Extension.Data.STEPS -> {
|
|
||||||
type = DataType.TYPE_STEP_COUNT_CUMULATIVE
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
runRequest(
|
|
||||||
DataReadRequest.Builder()
|
|
||||||
.read(type)
|
|
||||||
.setTimeRange(timeRange[0], timeRange[1], timeUnit)
|
|
||||||
.build(),
|
|
||||||
data
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runRequest(request: DataReadRequest, data: Extension.Data) {
|
|
||||||
Fitness.getHistoryClient(
|
|
||||||
activity,
|
|
||||||
GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
|
|
||||||
)
|
|
||||||
.readData(request)
|
|
||||||
.addOnSuccessListener { response ->
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
"Received response! ${response.dataSets.size} ${response.buckets.size} ${response.status}"
|
|
||||||
)
|
|
||||||
for (dataSet in response.dataSets) {
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"Data returned for Data type: ${dataSet.dataType.name} ${dataSet.dataPoints.size} ${dataSet.dataSource}"
|
|
||||||
)
|
|
||||||
dataSet.dataPoints.forEach { dp ->
|
|
||||||
|
|
||||||
// Global
|
|
||||||
Log.i(TAG, "Importing Data point:")
|
|
||||||
Log.i(TAG, "\tType: ${dp.dataType.name}")
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"\tStart: ${Date(dp.getStartTime(TimeUnit.SECONDS) * 1000L).toLocaleString()}"
|
|
||||||
)
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"\tEnd: ${Date(dp.getEndTime(TimeUnit.SECONDS) * 1000L).toLocaleString()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Field Specifics
|
|
||||||
for (field in dp.dataType.fields) {
|
|
||||||
Log.i(TAG, "\tField: ${field.name} Value: ${dp.getValue(field)}")
|
|
||||||
when (data) {
|
|
||||||
Extension.Data.WEIGHT -> {
|
|
||||||
val weight = Weight()
|
|
||||||
weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS)
|
|
||||||
weight.weight = dp.getValue(field).asFloat()
|
|
||||||
val list = weightLiveData.value?.list?.toMutableList()
|
|
||||||
?: ArrayList()
|
|
||||||
list.add(weight)
|
|
||||||
weightLiveData.value =
|
|
||||||
|
|
||||||
Extension.ImportState(Extension.States.WIP, list)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when (data) {
|
|
||||||
Extension.Data.WEIGHT -> {
|
|
||||||
weightLiveData.value =
|
|
||||||
Extension.ImportState(
|
|
||||||
Extension.States.DONE,
|
|
||||||
weightLiveData.value?.list
|
|
||||||
?: ArrayList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.addOnFailureListener { e ->
|
|
||||||
Log.e(TAG, "There was an error reading data from Google Fit", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
Log.d(this.name, "[$requestCode] -> [$resultCode]: $data")
|
|
||||||
if (requestCode == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultCode == Activity.RESULT_OK) connectLiveData.value = Extension.States.DONE
|
|
||||||
// signIn(Data.values()[requestCode])
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var weightLiveData: MutableLiveData<Extension.ImportState<Weight>>
|
|
||||||
|
|
||||||
override fun importWeight(): LiveData<Extension.ImportState<Weight>> {
|
|
||||||
|
|
||||||
weightLiveData = MutableLiveData(
|
|
||||||
Extension.ImportState(
|
|
||||||
Extension.States.WIP
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
startImport(Extension.Data.WEIGHT)
|
|
||||||
|
|
||||||
return weightLiveData
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,187 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.dzeio.openhealth.core.Observable
|
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
|
||||||
import com.dzeio.openhealth.utils.PermissionsManager
|
|
||||||
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.DataType
|
|
||||||
import com.google.android.gms.fitness.request.DataReadRequest
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.TimeZone
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class GoogleFitExtension : Extension {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "GoogleFitConnector"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val id = "GoogleFit"
|
|
||||||
override val name = "Google Fit"
|
|
||||||
|
|
||||||
override val permissions = arrayOf(
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
|
|
||||||
override val data: Array<Extension.Data> = arrayOf(
|
|
||||||
Extension.Data.WEIGHT
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun isConnected(): Boolean =
|
|
||||||
GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions)
|
|
||||||
|
|
||||||
|
|
||||||
private val fitnessOptions = FitnessOptions.builder()
|
|
||||||
.addDataType(DataType.TYPE_WEIGHT)
|
|
||||||
// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE)
|
|
||||||
// .addDataType(DataType.TYPE_CALORIES_EXPENDED)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val connectionStatus = Observable(false)
|
|
||||||
|
|
||||||
private lateinit var fragment: Fragment
|
|
||||||
|
|
||||||
override fun isAvailable(): Boolean = true
|
|
||||||
|
|
||||||
override fun enable(fragment: Fragment): Boolean {
|
|
||||||
this.fragment = fragment
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
override suspend fun connect(): Boolean {
|
|
||||||
|
|
||||||
if (isConnected()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return suspendCancellableCoroutine { cancellableContinuation ->
|
|
||||||
Log.d(this.name, "Signing In")
|
|
||||||
GoogleSignIn.requestPermissions(
|
|
||||||
fragment,
|
|
||||||
124887,
|
|
||||||
getGoogleAccount(), fitnessOptions
|
|
||||||
)
|
|
||||||
connectionStatus.addOneTimeObserver { it: Boolean ->
|
|
||||||
cancellableContinuation.resume(it) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val contract: ActivityResultContract<*, Map<String, @JvmSuppressWildcards Boolean>>? = ActivityResultContracts.RequestMultiplePermissions()
|
|
||||||
override val requestInput = permissions
|
|
||||||
|
|
||||||
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(fragment.requireContext(), fitnessOptions)
|
|
||||||
|
|
||||||
private val timeRange by lazy {
|
|
||||||
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
|
||||||
calendar.time = Date()
|
|
||||||
val endTime = calendar.timeInMillis
|
|
||||||
|
|
||||||
// Set year to 2013 to be sure to get data from when Google Fit Started to today
|
|
||||||
calendar.set(Calendar.YEAR, 2013)
|
|
||||||
val startTime = calendar.timeInMillis
|
|
||||||
return@lazy arrayOf(startTime, endTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun importWeight(): Flow<Extension.TaskProgress<ArrayList<Weight>>> =
|
|
||||||
channelFlow {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.INITIALIZATING
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val type = DataType.TYPE_WEIGHT
|
|
||||||
val timeUnit = TimeUnit.MILLISECONDS
|
|
||||||
|
|
||||||
val request = DataReadRequest.Builder()
|
|
||||||
.read(type)
|
|
||||||
.setTimeRange(timeRange[0], timeRange[1], timeUnit)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
Fitness.getHistoryClient(
|
|
||||||
fragment.requireContext(),
|
|
||||||
GoogleSignIn.getAccountForExtension(fragment.requireContext(), fitnessOptions)
|
|
||||||
)
|
|
||||||
.readData(request)
|
|
||||||
.addOnSuccessListener { response ->
|
|
||||||
val weights: ArrayList<Weight> = ArrayList()
|
|
||||||
var index = 0
|
|
||||||
var total = response.dataSets.size
|
|
||||||
for (dataset in response.dataSets) {
|
|
||||||
total += dataset.dataPoints.size - 1
|
|
||||||
for (dataPoint in dataset.dataPoints) {
|
|
||||||
total += dataPoint.dataType.fields.size - 1
|
|
||||||
for (field in dataPoint.dataType.fields) {
|
|
||||||
val weight = Weight().apply {
|
|
||||||
timestamp = dataPoint.getStartTime(TimeUnit.MILLISECONDS)
|
|
||||||
weight = dataPoint.getValue(field).asFloat()
|
|
||||||
source = this@GoogleFitExtension.id
|
|
||||||
}
|
|
||||||
weights.add(weight)
|
|
||||||
runBlocking {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.WORK_IN_PROGRESS,
|
|
||||||
progress = index++ / total.toFloat()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runBlocking {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.DONE,
|
|
||||||
additionalData = weights
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.addOnFailureListener {
|
|
||||||
runBlocking {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.ERROR,
|
|
||||||
statusMessage = it.localizedMessage ?: it.message ?: "Unknown error"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun exportWeights(weight: Array<Weight>): Flow<Extension.TaskProgress<Unit>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun permissionsGranted(): Boolean {
|
|
||||||
return PermissionsManager.hasPermission(this.fragment.requireContext(), permissions)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(result: Any) {
|
|
||||||
if ((result as Map<*, *>).containsValue(false)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionStatus.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.health.connect.client.HealthConnectClient
|
|
||||||
import androidx.health.connect.client.PermissionController
|
|
||||||
import androidx.health.connect.client.permission.HealthPermission
|
|
||||||
import androidx.health.connect.client.records.HeartRateRecord
|
|
||||||
import androidx.health.connect.client.records.StepsRecord
|
|
||||||
import androidx.health.connect.client.records.WeightRecord
|
|
||||||
import androidx.health.connect.client.request.ReadRecordsRequest
|
|
||||||
import androidx.health.connect.client.time.TimeRangeFilter
|
|
||||||
import com.dzeio.openhealth.core.Observable
|
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.P)
|
|
||||||
class HealthConnectExtension : Extension {
|
|
||||||
companion object {
|
|
||||||
const val TAG = "HealthConnectExtension"
|
|
||||||
}
|
|
||||||
|
|
||||||
// build a set of permissions for required data types
|
|
||||||
val PERMISSIONS =
|
|
||||||
setOf(
|
|
||||||
HealthPermission.createReadPermission(HeartRateRecord::class),
|
|
||||||
HealthPermission.createWritePermission(HeartRateRecord::class),
|
|
||||||
HealthPermission.createReadPermission(StepsRecord::class),
|
|
||||||
HealthPermission.createWritePermission(StepsRecord::class)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
override val id = "HealthConnect"
|
|
||||||
override val name = "Health Connect"
|
|
||||||
|
|
||||||
override val permissions = arrayOf(
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
|
|
||||||
override val requestInput = PERMISSIONS
|
|
||||||
|
|
||||||
override val data: Array<Extension.Data> = arrayOf(
|
|
||||||
Extension.Data.WEIGHT
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun isConnected(): Boolean = true
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private val connectionStatus = Observable(false)
|
|
||||||
|
|
||||||
private lateinit var fragment: Fragment
|
|
||||||
private lateinit var client: HealthConnectClient
|
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
|
||||||
return HealthConnectClient.isAvailable(fragment.requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun enable(fragment: Fragment): Boolean {
|
|
||||||
this.fragment = fragment
|
|
||||||
if (!isAvailable()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client = HealthConnectClient.getOrCreate(fragment.requireContext())
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun connect(): Boolean = true
|
|
||||||
|
|
||||||
override suspend fun importWeight(): Flow<Extension.TaskProgress<ArrayList<Weight>>> =
|
|
||||||
channelFlow {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.INITIALIZATING
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = client.readRecords(
|
|
||||||
ReadRecordsRequest(
|
|
||||||
WeightRecord::class,
|
|
||||||
timeRangeFilter = TimeRangeFilter.before(Instant.now())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val weights: ArrayList<Weight> = ArrayList()
|
|
||||||
var index = 0
|
|
||||||
for (record in response.records) {
|
|
||||||
val weight = Weight().apply {
|
|
||||||
timestamp = record.time.toEpochMilli()
|
|
||||||
weight = record.weight.inKilograms.toFloat()
|
|
||||||
source = this@HealthConnectExtension.id
|
|
||||||
}
|
|
||||||
weights.add(weight)
|
|
||||||
runBlocking {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.WORK_IN_PROGRESS,
|
|
||||||
progress = index++ / response.records.size.toFloat()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runBlocking {
|
|
||||||
send(
|
|
||||||
Extension.TaskProgress(
|
|
||||||
Extension.TaskState.DONE,
|
|
||||||
additionalData = weights
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun exportWeights(weight: Array<Weight>): Flow<Extension.TaskProgress<Unit>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(result: Any) {
|
|
||||||
if ((result as Set<*>).containsAll(this.PERMISSIONS)) connectionStatus.value = true
|
|
||||||
// signIn(Data.values()[requestCode])
|
|
||||||
}
|
|
||||||
|
|
||||||
override val contract: ActivityResultContract<Set<HealthPermission>, Set<HealthPermission>>
|
|
||||||
get() = PermissionController.createRequestPermissionResultContract()
|
|
||||||
|
|
||||||
override suspend fun permissionsGranted(): Boolean {
|
|
||||||
return this.client.permissionController.getGrantedPermissions(this.PERMISSIONS).containsAll(this.PERMISSIONS)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions.samsunghealth
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
|
||||||
import com.samsung.android.sdk.healthdata.HealthConnectionErrorResult
|
|
||||||
import com.samsung.android.sdk.healthdata.HealthConstants.StepCount
|
|
||||||
import com.samsung.android.sdk.healthdata.HealthDataStore
|
|
||||||
import com.samsung.android.sdk.healthdata.HealthDataStore.ConnectionListener
|
|
||||||
import com.samsung.android.sdk.healthdata.HealthPermissionManager
|
|
||||||
import com.samsung.android.sdk.healthdata.HealthPermissionManager.*
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Does not FUCKING work
|
|
||||||
*/
|
|
||||||
class SamsungHealth(
|
|
||||||
private val context: Activity
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "SamsungHealthConnector"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val listener = object : ConnectionListener {
|
|
||||||
override fun onConnected() {
|
|
||||||
Log.d(TAG, "Connected!")
|
|
||||||
if (isPermissionAcquired()) {
|
|
||||||
reporter.start()
|
|
||||||
} else {
|
|
||||||
requestPermission()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConnectionFailed(p0: HealthConnectionErrorResult?) {
|
|
||||||
Log.d(TAG, "Health data service is not available.")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisconnected() {
|
|
||||||
Log.d(TAG, "Health data service is disconnected.")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private val store: HealthDataStore = HealthDataStore(context, listener)
|
|
||||||
|
|
||||||
private fun isPermissionAcquired(): Boolean {
|
|
||||||
val permKey = PermissionKey(StepCount.HEALTH_DATA_TYPE, PermissionType.READ)
|
|
||||||
val pmsManager = HealthPermissionManager(store)
|
|
||||||
try {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
|
|
||||||
val sourceID: String = "SamsungHealth"
|
|
||||||
|
|
||||||
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) {
|
|
||||||
store.connectService()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
package com.dzeio.openhealth.extensions.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -53,6 +53,10 @@ class BrowseFragment :
|
|||||||
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome())
|
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.foodCalories.setOnClickListener {
|
||||||
|
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToFoodHomeFragment())
|
||||||
|
}
|
||||||
|
|
||||||
binding.steps.setOnClickListener {
|
binding.steps.setOnClickListener {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val activityPermission = PermissionsManager.hasPermission(
|
val activityPermission = PermissionsManager.hasPermission(
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
package com.dzeio.openhealth.ui.extension
|
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import com.dzeio.openhealth.core.BaseFragment
|
|
||||||
import com.dzeio.openhealth.databinding.FragmentExtensionBinding
|
|
||||||
import com.dzeio.openhealth.extensions.Extension
|
|
||||||
import com.dzeio.openhealth.extensions.ExtensionFactory
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ExtensionFragment :
|
|
||||||
BaseFragment<ExtensionViewModel, FragmentExtensionBinding>(ExtensionViewModel::class.java) {
|
|
||||||
|
|
||||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentExtensionBinding =
|
|
||||||
FragmentExtensionBinding::inflate
|
|
||||||
|
|
||||||
private val args: ExtensionFragmentArgs by navArgs()
|
|
||||||
|
|
||||||
private val extension by lazy {
|
|
||||||
ExtensionFactory.getExtension(args.extension)
|
|
||||||
?: throw Exception("No Extension found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var request: ActivityResultLauncher<Any>? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
if (this.extension.contract != null) {
|
|
||||||
this.request =
|
|
||||||
registerForActivityResult<Any, Any>(this.extension.contract!! as ActivityResultContract<Any, Any>) {
|
|
||||||
this.extension.onActivityResult(it as Any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onAttach(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val enabled = extension.enable(this)
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
throw Exception("Extension can't be enabled (${extension.id})")
|
|
||||||
}
|
|
||||||
|
|
||||||
requireActivity().actionBar?.title = extension.name
|
|
||||||
|
|
||||||
// extension.init(requireActivity())
|
|
||||||
|
|
||||||
binding.importButton.setOnClickListener {
|
|
||||||
val dialog = ProgressDialog(requireContext())
|
|
||||||
dialog.setTitle("Importing...")
|
|
||||||
dialog.setMessage("Imported 0 values")
|
|
||||||
dialog.show()
|
|
||||||
lifecycleScope.launch {
|
|
||||||
extension.importWeight().collectLatest { state ->
|
|
||||||
Log.d("ExtensionFragment", state.state.name)
|
|
||||||
dialog.setMessage(state.statusMessage ?: "progress ${state.progress}%")
|
|
||||||
if (state.state == Extension.TaskState.DONE) {
|
|
||||||
dialog.setMessage("Finishing Import...")
|
|
||||||
lifecycleScope.launchWhenStarted {
|
|
||||||
state.additionalData!!.forEach {
|
|
||||||
it.source = extension.id
|
|
||||||
viewModel.importWeight(it)
|
|
||||||
}
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
if (!extension.permissionsGranted() && request != null) {
|
|
||||||
request!!.launch(extension.requestInput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package com.dzeio.openhealth.ui.extension
|
|
||||||
|
|
||||||
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 javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ExtensionViewModel @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)
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package com.dzeio.openhealth.ui.extensions
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.dzeio.openhealth.adapters.ExtensionAdapter
|
|
||||||
import com.dzeio.openhealth.core.BaseFragment
|
|
||||||
import com.dzeio.openhealth.databinding.FragmentExtensionsBinding
|
|
||||||
import com.dzeio.openhealth.extensions.Extension
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ExtensionsFragment :
|
|
||||||
BaseFragment<ExtensionsViewModel, FragmentExtensionsBinding>(ExtensionsViewModel::class.java) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "ExtensionsFragment"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentExtensionsBinding =
|
|
||||||
FragmentExtensionsBinding::inflate
|
|
||||||
|
|
||||||
private lateinit var activeExtension: Extension
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val recycler = binding.list
|
|
||||||
|
|
||||||
val manager = LinearLayoutManager(requireContext())
|
|
||||||
recycler.layoutManager = manager
|
|
||||||
|
|
||||||
val adapter = ExtensionAdapter(viewModel.config)
|
|
||||||
adapter.onItemClick = {
|
|
||||||
activeExtension = it
|
|
||||||
activeExtension.enable(this)
|
|
||||||
Log.d(TAG, "${it.id}: ${it.name}")
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
extensionIsConnected(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recycler.adapter = adapter
|
|
||||||
|
|
||||||
val list = viewModel.extensions
|
|
||||||
list.forEach {
|
|
||||||
it.enable(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.set(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun extensionIsConnected(it: Extension) {
|
|
||||||
// check if it is connected
|
|
||||||
if (it.isConnected()) {
|
|
||||||
gotoExtension(it)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val ld = it.connect()
|
|
||||||
if (ld) {
|
|
||||||
gotoExtension(it)
|
|
||||||
}
|
|
||||||
// handle if extension can't be connected
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun gotoExtension(it: Extension) {
|
|
||||||
findNavController().navigate(
|
|
||||||
ExtensionsFragmentDirections.actionNavExtensionsToNavExtension(it.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package com.dzeio.openhealth.ui.extensions
|
|
||||||
|
|
||||||
import com.dzeio.openhealth.core.BaseViewModel
|
|
||||||
import com.dzeio.openhealth.extensions.ExtensionFactory
|
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class ExtensionsViewModel @Inject internal constructor(
|
|
||||||
val config: Configuration
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
val extensions = ExtensionFactory.getAll()
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,47 @@
|
|||||||
|
package com.dzeio.openhealth.ui.food
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.dzeio.openhealth.Application
|
||||||
|
import com.dzeio.openhealth.adapters.FoodAdapter
|
||||||
|
import com.dzeio.openhealth.core.BaseFragment
|
||||||
|
import com.dzeio.openhealth.databinding.FragmentFoodHomeBinding
|
||||||
|
import com.dzeio.openhealth.ui.steps.FoodHomeViewModel
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class FoodHomeFragment :
|
||||||
|
BaseFragment<FoodHomeViewModel, FragmentFoodHomeBinding>(FoodHomeViewModel::class.java) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "${Application.TAG}/SHFragment"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentFoodHomeBinding =
|
||||||
|
FragmentFoodHomeBinding::inflate
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewModel.init()
|
||||||
|
|
||||||
|
val recycler = binding.list
|
||||||
|
|
||||||
|
val manager = LinearLayoutManager(requireContext())
|
||||||
|
recycler.layoutManager = manager
|
||||||
|
|
||||||
|
val adapter = FoodAdapter()
|
||||||
|
adapter.onItemClick = {
|
||||||
|
// findNavController().navigate(
|
||||||
|
// WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterEdit(
|
||||||
|
// it.id
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
recycler.adapter = adapter
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.dzeio.openhealth.ui.steps
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.dzeio.openhealth.core.BaseViewModel
|
||||||
|
import com.dzeio.openhealth.data.food.Food
|
||||||
|
import com.dzeio.openhealth.data.food.FoodRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class FoodHomeViewModel @Inject internal constructor(
|
||||||
|
private val foodRepository: FoodRepository
|
||||||
|
) : BaseViewModel() {
|
||||||
|
val items: MutableLiveData<List<Food>> = MutableLiveData()
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
foodRepository.getAll().collectLatest {
|
||||||
|
items.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:weightSum="2">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/import_button"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="Force Import" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/export_button"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="Force Export" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/textView2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/extension_informations" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:id="@+id/list"
|
|
||||||
tools:listitem="@layout/layout_extension_item"
|
|
||||||
tools:context=".ui.extensions.ExtensionsFragment" />
|
|
37
app/src/main/res/layout/fragment_food_home.xml
Normal file
37
app/src/main/res/layout/fragment_food_home.xml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
style="?attr/materialCardViewFilledStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:id="@+id/list"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
tools:listitem="@layout/layout_item_list"
|
||||||
|
tools:context=".ui.weight.ListWeightFragment" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
48
app/src/main/res/layout/item_food.xml
Normal file
48
app/src/main/res/layout/item_food.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
style="?attr/materialCardViewFilledStyle"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/edit"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:weightSum="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/food_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Name of food" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/food_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="250g (800kcal)" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_baseline_edit_24" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
@ -29,16 +29,6 @@
|
|||||||
app:destination="@id/nav_weight_dialog" />
|
app:destination="@id/nav_weight_dialog" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_extensions"
|
|
||||||
android:name="com.dzeio.openhealth.ui.extensions.ExtensionsFragment"
|
|
||||||
android:label="@string/menu_extensions"
|
|
||||||
tools:layout="@layout/fragment_extensions">
|
|
||||||
<action
|
|
||||||
android:id="@+id/action_nav_extensions_to_nav_extension"
|
|
||||||
app:destination="@id/nav_extension" />
|
|
||||||
</fragment>
|
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/nav_list_weight"
|
android:id="@+id/nav_list_weight"
|
||||||
android:name="com.dzeio.openhealth.ui.weight.ListWeightFragment"
|
android:name="com.dzeio.openhealth.ui.weight.ListWeightFragment"
|
||||||
@ -89,7 +79,7 @@
|
|||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/nav_add_weight_dialog"
|
android:id="@+id/nav_add_weight_dialog"
|
||||||
android:name="com.dzeio.openhealth.ui.weight.AddWeightDialog"
|
android:name="com.dzeio.openhealth.ui.weight.WeightDialog"
|
||||||
tools:layout="@layout/dialog_water_size_selector">
|
tools:layout="@layout/dialog_water_size_selector">
|
||||||
|
|
||||||
</dialog>
|
</dialog>
|
||||||
@ -112,16 +102,6 @@
|
|||||||
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_extension"
|
|
||||||
android:name="com.dzeio.openhealth.ui.extension.ExtensionFragment"
|
|
||||||
tools:layout="@layout/fragment_extension">
|
|
||||||
|
|
||||||
<argument
|
|
||||||
android:name="extension"
|
|
||||||
app:argType="string" />
|
|
||||||
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/nav_about"
|
android:id="@+id/nav_about"
|
||||||
android:name="com.dzeio.openhealth.ui.about.AboutFragment"
|
android:name="com.dzeio.openhealth.ui.about.AboutFragment"
|
||||||
@ -141,6 +121,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_nav_browse_to_stepsHomeFragment"
|
android:id="@+id/action_nav_browse_to_stepsHomeFragment"
|
||||||
app:destination="@id/nav_steps_home" />
|
app:destination="@id/nav_steps_home" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_nav_browse_to_foodHomeFragment"
|
||||||
|
app:destination="@id/foodHomeFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
|
||||||
@ -165,4 +148,9 @@
|
|||||||
android:name="dialog_type"
|
android:name="dialog_type"
|
||||||
app:argType="integer" />
|
app:argType="integer" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/foodHomeFragment"
|
||||||
|
tools:layout="@layout/fragment_food_home"
|
||||||
|
android:name="com.dzeio.openhealth.ui.food.FoodHomeFragment"
|
||||||
|
android:label="FoodHomeFragment" />
|
||||||
</navigation>
|
</navigation>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user