1
0
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:
Florian Bouillon 2022-11-24 00:51:39 +01:00
parent e2b4ece9e4
commit 9d3585c42c
Signed by: Florian Bouillon
GPG Key ID: BEEAF3722D0EBF64
36 changed files with 589 additions and 1318 deletions

View File

@ -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 ;)).

View File

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

View 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')"
]
}
}

View File

@ -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"/>

View File

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

View File

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

View File

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

View File

@ -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"

View 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
)

View 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?>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />

View 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>

View 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>

View File

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