mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-22 10:52:13 +00:00
Merge branch 'master' into feat--Extensions
# Conflicts: # README.md
This commit is contained in:
commit
fd645f7e67
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@ -0,0 +1,15 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
[*.md]
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = android
|
10
README.md
10
README.md
@ -32,8 +32,10 @@ Permissions requests are for specifics usage and are only requests the first tim
|
||||
| ACCESS_FINE_LOCATION | Google Fit Extension Requirement (maybe not, still have to test) |
|
||||
| ACCESS_COARSE_LOCATION | Same as above |
|
||||
| ACTIVITY_RECOGNITION | Device Steps Usage |
|
||||
| INTERNET | Food fetching from OpenFoodFact |
|
||||
| POST_NOTIFICATIONS | send notifications for water intake and device steps usage |
|
||||
|
||||
No other permissions are used (even the internet permission ;)).
|
||||
No other permissions are used.
|
||||
|
||||
## Build
|
||||
|
||||
@ -42,6 +44,12 @@ No other permissions are used (even the internet permission ;)).
|
||||
- click on the debug icon for debug
|
||||
- it will be running on your emulator/device
|
||||
|
||||
## Design
|
||||
|
||||
If you want to contribute to the app design you can copy and edit the following Figma file
|
||||
|
||||
https://www.figma.com/file/AD63laksP2dvspRpT6MZ67/Open-Health?node-id=50995%3A3212&t=E5IFKMuqg8WDNQqc-1
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/dzeiocom/OpenHealth/blob/master/CONTRIBUTING.md)
|
||||
|
@ -1,8 +1,13 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
// Android Application?
|
||||
id("com.android.application")
|
||||
|
||||
// Support for kotlin in Android
|
||||
kotlin("android")
|
||||
|
||||
// Data Injection
|
||||
id("dagger.hilt.android.plugin")
|
||||
|
||||
// Safe Navigation
|
||||
@ -15,14 +20,45 @@ plugins {
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
// from: https://discuss.kotlinlang.org/t/use-git-hash-as-version-number-in-build-gradle-kts/19818/8
|
||||
fun String.runCommand(
|
||||
workingDir: File = File("."),
|
||||
timeoutAmount: Long = 60,
|
||||
timeoutUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): String = ProcessBuilder(split("\\s(?=(?:[^'\"`]*(['\"`])[^'\"`]*\\1)*[^'\"`]*$)".toRegex()))
|
||||
.directory(workingDir)
|
||||
.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||
.redirectError(ProcessBuilder.Redirect.PIPE)
|
||||
.start()
|
||||
.apply { waitFor(timeoutAmount, timeoutUnit) }
|
||||
.run {
|
||||
val error = errorStream.bufferedReader().readText().trim()
|
||||
if (error.isNotEmpty()) {
|
||||
return@run ""
|
||||
}
|
||||
inputStream.bufferedReader().readText().trim()
|
||||
}
|
||||
|
||||
// The application ID
|
||||
val appID = "com.dzeio.openhealth"
|
||||
|
||||
// Languages
|
||||
// the application supported languages
|
||||
val locales = listOf("en", "fr")
|
||||
|
||||
// minimum application required SDK version to run
|
||||
val sdkMin = 21
|
||||
|
||||
// target SDK version
|
||||
val sdkTarget = 33
|
||||
|
||||
val branch = "git rev-parse --abbrev-ref HEAD".runCommand(workingDir = rootDir)
|
||||
var tag = "git tag -l --points-at HEAD".runCommand(workingDir = rootDir)
|
||||
if (tag == "") {
|
||||
tag = "not tagged"
|
||||
}
|
||||
val commitId = "git rev-parse HEAD".runCommand(workingDir = rootDir)
|
||||
.subSequence(0, 7)
|
||||
|
||||
android {
|
||||
|
||||
signingConfigs {
|
||||
@ -41,7 +77,6 @@ android {
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +93,7 @@ android {
|
||||
targetSdk = sdkTarget
|
||||
|
||||
// Semantic Versioning
|
||||
versionName = "1.0.0"
|
||||
versionName = "0.1.0"
|
||||
versionCode = 1
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
@ -75,6 +110,10 @@ android {
|
||||
"new String[]{\"" + locales.joinToString("\",\"") + "\"}"
|
||||
)
|
||||
resourceConfigurations += locales
|
||||
|
||||
buildConfigField("String", "BRANCH", "\"$branch\"")
|
||||
buildConfigField("String", "TAG", "\"$tag\"")
|
||||
buildConfigField("String", "COMMIT", "\"$commitId\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -82,12 +121,18 @@ android {
|
||||
getByName("release") {
|
||||
// Slimmer version
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
isShrinkResources = true
|
||||
isDebuggable = false
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
|
||||
getByName("debug") {
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
applicationIdSuffix = ".dev"
|
||||
versionNameSuffix = "-dev"
|
||||
isDebuggable = true
|
||||
@ -118,24 +163,26 @@ android {
|
||||
namespace = appID
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Dzeio Charts
|
||||
implementation(project(":charts"))
|
||||
// implementation(project(":CrashHandler"))
|
||||
implementation("com.dzeio:charts:fe20f90654")
|
||||
|
||||
// Dzeio Crash Handler
|
||||
implementation("com.dzeio:crashhandler:1.0.1")
|
||||
|
||||
// Core dependencies
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0-alpha01")
|
||||
implementation("javax.inject:javax.inject:1")
|
||||
implementation("com.google.android.material:material:1.8.0-alpha02")
|
||||
implementation("com.google.android.material:material:1.9.0-alpha01")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
||||
|
||||
// implementation("com.github.Aviortheking:crashhandler:0.2.3")
|
||||
|
||||
// Coroutines
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
|
||||
|
||||
@ -153,39 +200,24 @@ dependencies {
|
||||
implementation("androidx.paging:paging-runtime:3.1.1")
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.1.1")
|
||||
|
||||
|
||||
// Services
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
|
||||
// Graph
|
||||
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
|
||||
|
||||
// Graphs test 2
|
||||
implementation("com.github.HackPlan:AndroidCharts:1.0.4")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
|
||||
// Hilt
|
||||
implementation("com.google.dagger:hilt-android:2.43.2")
|
||||
kapt("com.google.dagger:hilt-compiler:2.43.2")
|
||||
|
||||
// Google Fit
|
||||
implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
||||
implementation("com.google.android.gms:play-services-auth:20.3.0")
|
||||
implementation("androidx.health.connect:connect-client:1.0.0-alpha07")
|
||||
|
||||
// Samsung Health
|
||||
implementation(files("libs/samsung-health-data-1.5.0.aar"))
|
||||
implementation("com.google.code.gson:gson:2.9.1")
|
||||
implementation("com.google.dagger:hilt-android:2.44.2")
|
||||
kapt("com.google.dagger:hilt-compiler:2.44.2")
|
||||
|
||||
// ROOM
|
||||
implementation("androidx.room:room-runtime:2.4.3")
|
||||
kapt("androidx.room:room-compiler:2.4.3")
|
||||
implementation("androidx.room:room-ktx:2.4.3")
|
||||
testImplementation("androidx.room:room-testing:2.4.3")
|
||||
implementation("androidx.room:room-runtime:2.5.0")
|
||||
kapt("androidx.room:room-compiler:2.5.0")
|
||||
implementation("androidx.room:room-ktx:2.5.0")
|
||||
testImplementation("androidx.room:room-testing:2.5.0")
|
||||
|
||||
// Futures
|
||||
implementation("com.google.guava:guava:31.1-jre")
|
||||
@ -194,4 +226,9 @@ dependencies {
|
||||
|
||||
// OSS Licenses
|
||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||
|
||||
// Retrofit (Open Food Fact)
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
|
||||
}
|
||||
|
Binary file not shown.
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@ -18,4 +18,9 @@
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
# do not obfuscate fields with the annotation `@SerializedName`
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "2acd5897bbf15393886259605a1df934",
|
||||
"identityHash": "414712cc283c7f1d14cde8e00da277fb",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Weight",
|
||||
@ -34,10 +34,10 @@
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
@ -82,10 +82,10 @@
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
@ -130,10 +130,10 @@
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
@ -147,12 +147,86 @@
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Food",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `serving` TEXT NOT NULL, `quantity` REAL NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `image` TEXT, `timestamp` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serving",
|
||||
"columnName": "serving",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "quantity",
|
||||
"columnName": "quantity",
|
||||
"affinity": "REAL",
|
||||
"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": "image",
|
||||
"columnName": "image",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"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, '2acd5897bbf15393886259605a1df934')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '414712cc283c7f1d14cde8e00da277fb')"
|
||||
]
|
||||
}
|
||||
}
|
274
app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json
Normal file
274
app/schemas/com.dzeio.openhealth.data.AppDatabase/2.json
Normal file
@ -0,0 +1,274 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "68d8da27dde7a13bc063b8c3d8d34e5b",
|
||||
"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, `bmi` REAL, `totalBodyWater` REAL, `muscles` REAL, `leanBodyMass` REAL, `bodyFat` REAL, `boneMass` REAL, `visceralFat` REAL)",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"fieldPath": "bmi",
|
||||
"columnName": "bmi",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "totalBodyWater",
|
||||
"columnName": "totalBodyWater",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "muscles",
|
||||
"columnName": "muscles",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "leanBodyMass",
|
||||
"columnName": "leanBodyMass",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "bodyFat",
|
||||
"columnName": "bodyFat",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "boneMass",
|
||||
"columnName": "boneMass",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "visceralFat",
|
||||
"columnName": "visceralFat",
|
||||
"affinity": "REAL",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"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, `serving` TEXT NOT NULL, `quantity` REAL NOT NULL, `proteins` REAL NOT NULL, `carbohydrates` REAL NOT NULL, `fat` REAL NOT NULL, `energy` REAL NOT NULL, `image` TEXT, `timestamp` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serving",
|
||||
"columnName": "serving",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "quantity",
|
||||
"columnName": "quantity",
|
||||
"affinity": "REAL",
|
||||
"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": "image",
|
||||
"columnName": "image",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"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, '68d8da27dde7a13bc063b8c3d8d34e5b')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
package com.dzeio.openhealth
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.dzeio.openhealth", appContext.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
app/src/debug/res/drawable/baseline_chevron_left_24.xml
Normal file
5
app/src/debug/res/drawable/baseline_chevron_left_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#000000" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
|
||||
</vector>
|
5
app/src/debug/res/drawable/baseline_chevron_right_24.xml
Normal file
5
app/src/debug/res/drawable/baseline_chevron_right_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#000000" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
|
||||
</vector>
|
@ -1,22 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Internet for OFF -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- Notifications -->
|
||||
<!-- Notifications for Water and Service -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- Google Fit -->
|
||||
<!-- Phone Sensors for Steps -->
|
||||
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
||||
|
||||
<!-- Bluetooth Connection -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- Phone Sensors -->
|
||||
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
||||
|
||||
<!-- Samsung Health-->
|
||||
<queries>
|
||||
<package android:name="com.sec.android.app.shealth" />
|
||||
</queries>
|
||||
<uses-sdk tools:overrideLibrary="androidx.health.connect.client" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
@ -27,16 +26,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.OpenHealth">
|
||||
|
||||
<!-- Samsung Health-->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.health.permission.read"
|
||||
android:value="com.samsung.health.step_count" />
|
||||
|
||||
<!-- Google Fit -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version" />
|
||||
|
||||
<!-- TODO: Respect what daddy Google wants and try to remove the SplashScreen -->
|
||||
<!-- Main Activity duh -->
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
@ -48,6 +39,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- CrashHandler -->
|
||||
<activity android:name=".ui.ErrorActivity"
|
||||
android:theme="@style/Theme.OpenHealth.NoActionBar"
|
||||
android:exported="false" />
|
||||
@ -60,26 +52,12 @@
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||
android:theme="@style/Theme.OpenHealth" />
|
||||
|
||||
<!-- Activity to show rationale of Health Connect permissions -->
|
||||
<activity
|
||||
android:name=".ui.PrivacyPolicyActivity"
|
||||
android:exported="true"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||
</intent-filter>
|
||||
<!-- List of health data permissions -->
|
||||
<meta-data
|
||||
android:name="health_permissions"
|
||||
android:resource="@array/health_permissions" />
|
||||
</activity>
|
||||
|
||||
|
||||
|
||||
<!-- the Service for the application -->
|
||||
<service
|
||||
android:name=".services.OpenHealthService"
|
||||
android:permission="android.permission.ACTIVITY_RECOGNITION" />
|
||||
|
||||
<!-- Android 13 Locales management if I remember correctly -->
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
@ -92,10 +70,4 @@
|
||||
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.apps.healthdata" />
|
||||
</queries>
|
||||
|
||||
|
||||
|
||||
</manifest>
|
||||
|
@ -16,24 +16,36 @@ class Application : Application() {
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// setup the CrashHandler
|
||||
CrashHandler.Builder()
|
||||
.withActivity(ErrorActivity::class.java)
|
||||
.withPrefs(prefs)
|
||||
.witheErrorReporterCrashKey(R.string.error_reporter_crash)
|
||||
.withPrefsKey(Settings.CRASH_LAST_TIME)
|
||||
.withPrefix("${BuildConfig.APPLICATION_ID} v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
.withPrefix(
|
||||
"""
|
||||
${BuildConfig.APPLICATION_ID} v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
||||
|
||||
Build informations:
|
||||
Commit: ${BuildConfig.COMMIT}
|
||||
Branch: ${BuildConfig.BRANCH}
|
||||
Tag: ${BuildConfig.TAG}
|
||||
""".trimIndent()
|
||||
)
|
||||
.build()
|
||||
.setup(this)
|
||||
|
||||
// Android Dynamics Colors
|
||||
// setup for Android Dynamics Colors
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the language of the application if said in the settings
|
||||
*/
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(LocaleUtils.onAttach(base))
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
package com.dzeio.openhealth
|
||||
|
||||
import com.dzeio.openhealth.extensions.Extension
|
||||
|
||||
/**
|
||||
* Object containing every keys for the different settings of the application
|
||||
*/
|
||||
object Settings {
|
||||
|
||||
/**
|
||||
@ -29,8 +30,8 @@ object Settings {
|
||||
*/
|
||||
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
|
||||
|
||||
fun extensionEnabled(extension: Extension): String {
|
||||
return "com.dzeio.open-health.extension.${extension.id}.enabled"
|
||||
}
|
||||
|
||||
/**
|
||||
* Goal number of steps each days
|
||||
*/
|
||||
const val STEPS_GOAL = "com.dzeio.open-health.steps.goal-daily"
|
||||
}
|
||||
|
@ -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,45 @@
|
||||
package com.dzeio.openhealth.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.dzeio.openhealth.R
|
||||
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
|
||||
import com.dzeio.openhealth.utils.NetworkUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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
|
||||
) {
|
||||
// Download remote picture
|
||||
if (item.image != null) {
|
||||
NetworkUtils.getImageInBackground(holder.binding.productImage, item.image!!)
|
||||
}
|
||||
|
||||
// set the food name
|
||||
holder.binding.foodName.text = item.name + " ${item.id}"
|
||||
|
||||
// set the food description
|
||||
holder.binding.foodDescription.text = holder.itemView.context.getString(
|
||||
R.string.food_description,
|
||||
item.quantity.roundToInt().toString() + item.serving.replace(Regex("\\d+"), ""),
|
||||
(item.energy / 100 * item.quantity)
|
||||
)
|
||||
|
||||
// set the callback
|
||||
holder.binding.edit.setOnClickListener {
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.dzeio.openhealth.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.dzeio.openhealth.core.BaseAdapter
|
||||
import com.dzeio.openhealth.core.BaseViewHolder
|
||||
import com.dzeio.openhealth.databinding.ItemListBinding
|
||||
import com.dzeio.openhealth.utils.NetworkUtils
|
||||
|
||||
class ItemAdapter<T> : BaseAdapter<ItemAdapter.Item<T>, ItemListBinding>() {
|
||||
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> ItemListBinding
|
||||
get() = ItemListBinding::inflate
|
||||
|
||||
var onItemClick: ((weight: Item<T>) -> Unit)? = null
|
||||
|
||||
override fun onBindData(
|
||||
holder: BaseViewHolder<ItemListBinding>,
|
||||
item: Item<T>,
|
||||
position: Int
|
||||
) {
|
||||
val binding = holder.binding
|
||||
|
||||
binding.title.text = item.title
|
||||
binding.subValue.text = item.subtitle
|
||||
|
||||
if (item.image != null) {
|
||||
NetworkUtils.getImageInBackground(binding.image, item.image)
|
||||
} else {
|
||||
binding.image.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (item.icon != null) {
|
||||
binding.iconRight.setImageResource(item.icon)
|
||||
} else {
|
||||
binding.iconRight.visibility = View.GONE
|
||||
}
|
||||
|
||||
// set the callback
|
||||
binding.card.setOnClickListener {
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
}
|
||||
|
||||
data class Item<T>(
|
||||
val value: T,
|
||||
val title: String? = null,
|
||||
val subtitle: String? = null,
|
||||
val image: String? = null,
|
||||
@DrawableRes
|
||||
val icon: Int? = null
|
||||
)
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package com.dzeio.openhealth.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseAdapter
|
||||
import com.dzeio.openhealth.core.BaseViewHolder
|
||||
import com.dzeio.openhealth.data.step.Step
|
||||
@ -14,13 +16,29 @@ class StepsAdapter() : BaseAdapter<Step, LayoutItemListBinding>() {
|
||||
|
||||
var onItemClick: ((weight: Step) -> Unit)? = null
|
||||
|
||||
var isDay = false
|
||||
|
||||
override fun onBindData(
|
||||
holder: BaseViewHolder<LayoutItemListBinding>,
|
||||
item: Step,
|
||||
position: Int
|
||||
) {
|
||||
holder.binding.value.text = "${item.value}steps"
|
||||
holder.binding.datetime.text = item.formatTimestamp()
|
||||
// set the number of steps taken
|
||||
holder.binding.value.text = holder.itemView.context.getString(
|
||||
R.string.steps_count,
|
||||
item.value
|
||||
)
|
||||
|
||||
// set the datetime
|
||||
holder.binding.datetime.text = item.formatTimestamp(!isDay)
|
||||
|
||||
if (isDay) {
|
||||
holder.binding.iconRight.visibility = View.GONE
|
||||
} else {
|
||||
holder.binding.iconRight.setImageResource(R.drawable.ic_zoom_out_map)
|
||||
}
|
||||
|
||||
// set the callback
|
||||
holder.binding.edit.setOnClickListener {
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
|
@ -6,8 +6,14 @@ import com.dzeio.openhealth.core.BaseAdapter
|
||||
import com.dzeio.openhealth.core.BaseViewHolder
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
import com.dzeio.openhealth.databinding.LayoutItemListBinding
|
||||
import com.dzeio.openhealth.units.Units
|
||||
|
||||
class WaterAdapter() : BaseAdapter<Water, LayoutItemListBinding>() {
|
||||
class WaterAdapter : BaseAdapter<Water, LayoutItemListBinding>() {
|
||||
|
||||
/**
|
||||
* The unit the adapter will be using
|
||||
*/
|
||||
var unit: Units.Volume = Units.Volume.MILLILITER
|
||||
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutItemListBinding
|
||||
get() = LayoutItemListBinding::inflate
|
||||
@ -19,8 +25,14 @@ class WaterAdapter() : BaseAdapter<Water, LayoutItemListBinding>() {
|
||||
item: Water,
|
||||
position: Int
|
||||
) {
|
||||
holder.binding.value.text = "${item.value}ml"
|
||||
holder.binding.datetime.text = "${item.formatTimestamp()}"
|
||||
// set the wate intake text
|
||||
holder.binding.value.text =
|
||||
holder.itemView.context.getString(unit.unit, unit.formatToString(item.value))
|
||||
|
||||
// set the datetime
|
||||
holder.binding.datetime.text = item.formatTimestamp()
|
||||
|
||||
// set the callback
|
||||
holder.binding.edit.setOnClickListener {
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ import com.dzeio.openhealth.units.Units
|
||||
|
||||
class WeightAdapter : BaseAdapter<Weight, LayoutItemListBinding>() {
|
||||
|
||||
/**
|
||||
* The unit the adapter will be using
|
||||
*/
|
||||
var unit: Units.Mass = Units.Mass.KILOGRAM
|
||||
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutItemListBinding
|
||||
@ -22,12 +25,14 @@ class WeightAdapter : BaseAdapter<Weight, LayoutItemListBinding>() {
|
||||
item: Weight,
|
||||
position: Int
|
||||
) {
|
||||
|
||||
|
||||
|
||||
// set the weight text
|
||||
holder.binding.value.text =
|
||||
holder.itemView.context.getString(unit.unit, unit.formatToString(item.weight))
|
||||
|
||||
// set the datetime
|
||||
holder.binding.datetime.text = item.formatTimestamp()
|
||||
|
||||
// set the callback
|
||||
holder.binding.edit.setOnClickListener {
|
||||
onItemClick?.invoke(item)
|
||||
}
|
||||
|
@ -5,9 +5,11 @@ import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Base around the Activity class to simplify usage
|
||||
*/
|
||||
abstract class BaseActivity<VB : ViewBinding>() : AppCompatActivity() {
|
||||
|
||||
|
||||
/**
|
||||
* Function to inflate the Fragment Bindings
|
||||
*
|
||||
@ -28,4 +30,4 @@ abstract class BaseActivity<VB : ViewBinding>() : AppCompatActivity() {
|
||||
}
|
||||
|
||||
protected open fun onCreated(savedInstanceState: Bundle?) {}
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,11 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Base around the adapter to simplify usage
|
||||
*/
|
||||
abstract class BaseAdapter<T, VB : ViewBinding> : RecyclerView.Adapter<BaseViewHolder<VB>>() {
|
||||
private var items = mutableListOf<T>()
|
||||
// private var lastPosition = -1
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun set(items: List<T>) {
|
||||
@ -22,6 +24,12 @@ abstract class BaseAdapter<T, VB : ViewBinding> : RecyclerView.Adapter<BaseViewH
|
||||
notifyItemInserted(len)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val len = this.items.size
|
||||
this.items.clear()
|
||||
notifyItemRangeRemoved(0, len)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to inflate the Adapter Bindings
|
||||
*
|
||||
@ -29,6 +37,9 @@ abstract class BaseAdapter<T, VB : ViewBinding> : RecyclerView.Adapter<BaseViewH
|
||||
*/
|
||||
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
|
||||
|
||||
/**
|
||||
* function run when an item is displayed
|
||||
*/
|
||||
abstract fun onBindData(holder: BaseViewHolder<VB>, item: T, position: Int)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<VB> {
|
||||
@ -42,5 +53,4 @@ abstract class BaseAdapter<T, VB : ViewBinding> : RecyclerView.Adapter<BaseViewH
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Update
|
||||
|
||||
|
||||
/**
|
||||
* Base for a DAO interface
|
||||
*/
|
||||
interface BaseDao<T> {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg obj: T): List<Long>
|
||||
@ -13,9 +15,12 @@ interface BaseDao<T> {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(obj: T): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(vararg obj: T)
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg obj: T)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg obj: T)
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ import androidx.viewbinding.ViewBinding
|
||||
*
|
||||
* note: Dialog crash app with viewmodel error? add @AndroidEntryPoint
|
||||
*/
|
||||
abstract class BaseDialog<VM : BaseViewModel, VB : ViewBinding>(private val viewModelClass: Class<VM>) :
|
||||
abstract class BaseDialog<VM : BaseViewModel, VB : ViewBinding>(
|
||||
private val viewModelClass: Class<VM>
|
||||
) :
|
||||
BaseSimpleDialog<VB>() {
|
||||
|
||||
val viewModel by lazy {
|
||||
|
@ -3,6 +3,9 @@ package com.dzeio.openhealth.core
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Base around the Fragment class to simplify usage
|
||||
*/
|
||||
abstract class BaseFragment<VM : BaseViewModel, VB : ViewBinding>(
|
||||
private val viewModelClass: Class<VM>
|
||||
) :
|
||||
|
@ -13,8 +13,16 @@ import androidx.viewbinding.ViewBinding
|
||||
import com.dzeio.openhealth.R
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(private val viewModelClass: Class<VM>) : DialogFragment() {
|
||||
/**
|
||||
* Base around the DialogFragment class to simplify usage
|
||||
*/
|
||||
abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(
|
||||
private val viewModelClass: Class<VM>
|
||||
) : DialogFragment() {
|
||||
|
||||
/**
|
||||
* Lazyload the viewModel
|
||||
*/
|
||||
val viewModel by lazy {
|
||||
ViewModelProvider(this)[viewModelClass]
|
||||
}
|
||||
@ -22,14 +30,14 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
|
||||
private var _binding: VB? = null
|
||||
val binding get() = _binding!!
|
||||
|
||||
|
||||
/**
|
||||
* Function to inflate the Fragment Bindings
|
||||
*/
|
||||
abstract val bindingInflater: (LayoutInflater) -> VB
|
||||
|
||||
abstract val isFullscreenLayout: Boolean
|
||||
|
||||
/**
|
||||
* Function run when the dialog was created
|
||||
*/
|
||||
open fun onCreated(savedInstanceState: Bundle?) {}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -37,7 +45,6 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
_binding = bindingInflater(inflater)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
@ -59,14 +66,18 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
|
||||
|
||||
onDialogInit(dialog)
|
||||
|
||||
// onCreated()
|
||||
|
||||
dialog
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
||||
open fun onDialogInit(dialog: Dialog): Unit {}
|
||||
/**
|
||||
* Function to modify the Dialog
|
||||
*/
|
||||
open fun onDialogInit(dialog: Dialog) {}
|
||||
|
||||
/**
|
||||
* FIXME: Remove it from the Base and put it in the implementations
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
@ -75,10 +86,6 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy binding
|
||||
*/
|
||||
@ -86,4 +93,4 @@ abstract class BaseFullscreenDialog<VM : BaseViewModel, VB : ViewBinding>(privat
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,16 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
/**
|
||||
* Base around the DialogFragment class to simplify usage
|
||||
*/
|
||||
abstract class BaseSimpleDialog<VB : ViewBinding> : DialogFragment() {
|
||||
|
||||
/**
|
||||
* Function to inflate the Fragment Bindings
|
||||
*/
|
||||
abstract val bindingInflater: (LayoutInflater) -> VB
|
||||
|
||||
private var _binding: VB? = null
|
||||
val binding get() = _binding!!
|
||||
|
||||
@ -36,16 +44,20 @@ abstract class BaseSimpleDialog<VB : ViewBinding> : DialogFragment() {
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to add more customization to the AlertDialogBuilder
|
||||
*/
|
||||
open fun onBuilderInit(builder: MaterialAlertDialogBuilder) {}
|
||||
|
||||
/**
|
||||
* Function that allow to modificate some elements of the final dialog
|
||||
*/
|
||||
open fun onDialogInit(dialog: AlertDialog) {}
|
||||
|
||||
open fun onCreated() {}
|
||||
|
||||
/**
|
||||
* Function to inflate the Fragment Bindings
|
||||
* Function run when the dialog is created
|
||||
*/
|
||||
abstract val bindingInflater: (LayoutInflater) -> VB
|
||||
open fun onCreated() {}
|
||||
|
||||
/**
|
||||
* Destroy binding
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.dzeio.openhealth.core
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -8,11 +7,23 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Base around the Fragment class to simplify usage
|
||||
*
|
||||
* Without ViewModel support (use `BaseFragment` instead)
|
||||
*/
|
||||
abstract class BaseStaticFragment<VB : ViewBinding> : Fragment() {
|
||||
|
||||
private var _binding: VB? = null
|
||||
val binding get() = _binding!!
|
||||
|
||||
/**
|
||||
* Function to inflate the Fragment Bindings
|
||||
*
|
||||
* use like this: `ViewBinding::inflater`
|
||||
*/
|
||||
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
|
||||
|
||||
/**
|
||||
* Setup everything!
|
||||
*/
|
||||
@ -28,13 +39,6 @@ abstract class BaseStaticFragment<VB : ViewBinding> : Fragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to inflate the Fragment Bindings
|
||||
*
|
||||
* use like this: `ViewBinding::inflater`
|
||||
*/
|
||||
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
|
||||
|
||||
/**
|
||||
* Destroy binding
|
||||
*/
|
||||
|
@ -3,7 +3,9 @@ package com.dzeio.openhealth.core
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Simple implementation of RecyclerView.ViewHolder to limitate usage
|
||||
*/
|
||||
class BaseViewHolder<VB : ViewBinding>(
|
||||
val binding : VB
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
}
|
||||
val binding: VB
|
||||
) : RecyclerView.ViewHolder(binding.root)
|
||||
|
@ -2,4 +2,7 @@ package com.dzeio.openhealth.core
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
abstract class BaseViewModel : ViewModel()
|
||||
/**
|
||||
* Simple Extension of the base ViewModel
|
||||
*/
|
||||
abstract class BaseViewModel : ViewModel()
|
||||
|
@ -9,6 +9,9 @@ import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.dzeio.openhealth.Application
|
||||
|
||||
/**
|
||||
* Worker Wrapper to simplify work and usage
|
||||
*/
|
||||
abstract class BaseWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
|
||||
companion object {
|
||||
@ -19,4 +22,4 @@ abstract class BaseWorker(context: Context, params: WorkerParameters) : Worker(c
|
||||
.enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,16 @@ package com.dzeio.openhealth.core
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
/**
|
||||
* Simple Observable implementation
|
||||
*/
|
||||
open class Observable<T>(baseValue: T) {
|
||||
|
||||
private val functionObservers: ArrayList<(T) -> Unit> = ArrayList()
|
||||
|
||||
|
||||
fun addObserver(fn: (T) -> Unit) {
|
||||
if (!functionObservers.contains(fn)) {
|
||||
functionObservers.add(fn)
|
||||
@ -35,7 +39,6 @@ open class Observable<T>(baseValue: T) {
|
||||
}
|
||||
|
||||
fun notifyObservers() {
|
||||
|
||||
// Notify Functions
|
||||
for (fn in functionObservers) {
|
||||
notifyObserver(fn)
|
||||
@ -53,4 +56,19 @@ open class Observable<T>(baseValue: T) {
|
||||
}
|
||||
return ld
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the observable to a Kotlin Channel
|
||||
*/
|
||||
fun toChannel(): Channel<T> = Channel<T>(Channel.RENDEZVOUS).apply {
|
||||
addObserver {
|
||||
trySend(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun toFlow() = callbackFlow {
|
||||
addObserver {
|
||||
trySend(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
package com.dzeio.openhealth.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
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.StepDao
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
@ -11,50 +14,55 @@ import com.dzeio.openhealth.data.water.WaterDao
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.data.weight.WeightDao
|
||||
|
||||
/**
|
||||
* ROOM SQLite database for the application
|
||||
*
|
||||
* It may be replaced if I want to fully encrypt the database
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
Weight::class,
|
||||
Water::class,
|
||||
Step::class
|
||||
Step::class,
|
||||
Food::class
|
||||
],
|
||||
version = 2,
|
||||
autoMigrations = [
|
||||
AutoMigration(1, 2)
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
// private val PREPOPULATE_DATA = listOf(Thing("1", "val"), Thing("2", "val 2"))
|
||||
|
||||
abstract fun weightDao(): WeightDao
|
||||
abstract fun waterDao(): WaterDao
|
||||
abstract fun stepDao(): StepDao
|
||||
|
||||
abstract fun foodDao(): FoodDao
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* database name duh
|
||||
*/
|
||||
private const val DATABASE_NAME = "open_health"
|
||||
|
||||
// For Singleton instantiation
|
||||
@Volatile
|
||||
private var instance: AppDatabase? = null
|
||||
|
||||
// get the Database instance
|
||||
fun getInstance(context: Context): AppDatabase {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: buildDatabase(context).also { instance = it }
|
||||
}
|
||||
}
|
||||
|
||||
// Create and pre-populate the database. See this article for more details:
|
||||
// https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
|
||||
/**
|
||||
* build teh database
|
||||
*/
|
||||
private fun buildDatabase(context: Context): AppDatabase {
|
||||
return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
|
||||
// .addCallback(object : Callback() {
|
||||
// override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
// super.onCreate(db)
|
||||
// // moving to a new thread
|
||||
// Executors.newSingleThreadExecutor().execute {
|
||||
// getInstance(context).thingDao()
|
||||
// .insert(PREPOPULATE_DATA)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .addMigrations(MIGRATION_2_3)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
package com.dzeio.openhealth.data.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object TiviTypeConverters {
|
||||
private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun toOffsetDateTime(value: String?): OffsetDateTime? {
|
||||
return value?.let {
|
||||
return formatter.parse(value, OffsetDateTime::from)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromOffsetDateTime(date: OffsetDateTime?): String? {
|
||||
return date?.format(formatter)
|
||||
}
|
||||
}
|
107
app/src/main/java/com/dzeio/openhealth/data/food/Food.kt
Normal file
107
app/src/main/java/com/dzeio/openhealth/data/food/Food.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package com.dzeio.openhealth.data.food
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.dzeio.openhealth.data.openfoodfact.OFFProduct
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
|
||||
@Entity
|
||||
data class Food(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
|
||||
/**
|
||||
* The product name
|
||||
*/
|
||||
var name: String,
|
||||
|
||||
/**
|
||||
* The product serving text
|
||||
*
|
||||
* ex: `250ml`, `520g`, etc
|
||||
*/
|
||||
var serving: String,
|
||||
|
||||
/**
|
||||
* the quantity taken by the user
|
||||
*/
|
||||
var quantity: Float,
|
||||
|
||||
/**
|
||||
* the quantity of proteins there is for 100 quantity
|
||||
*/
|
||||
var proteins: Float,
|
||||
|
||||
/**
|
||||
* the quantity of carbohydrates there is for 100 quantity
|
||||
*/
|
||||
var carbohydrates: Float,
|
||||
|
||||
/**
|
||||
* the quantity of fat there is for 100 quantity
|
||||
*/
|
||||
var fat: Float,
|
||||
|
||||
/**
|
||||
* the quantity of energy there is for 100 quantity
|
||||
*/
|
||||
var energy: Float,
|
||||
|
||||
/**
|
||||
* the url of the image of the product
|
||||
*/
|
||||
var image: String?,
|
||||
|
||||
/**
|
||||
* When the entry was added to our Database
|
||||
*/
|
||||
var timestamp: Long = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis
|
||||
) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Transform an OpenFoodFact product to use for our Database
|
||||
*/
|
||||
fun fromOpenFoodFact(food: OFFProduct, quantity: Float? = null): Food? {
|
||||
// filter out foods that we can't use in the app
|
||||
if (
|
||||
food.nutriments == null ||
|
||||
food.name == null ||
|
||||
((food.servingSize == null || food.servingSize == "") && (food.quantity == null || food.quantity == "") && food.servingQuantity == null && food.productQuantity == null)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// try to know how much was eaten by the user if not said
|
||||
var eaten = quantity ?: food.servingQuantity ?: food.productQuantity ?: 0f
|
||||
if (eaten == 0f) {
|
||||
if (food.servingQuantity != null && food.servingQuantity != 0f) {
|
||||
eaten = food.servingQuantity!!
|
||||
} else if (food.productQuantity != null && food.productQuantity != 0f) {
|
||||
eaten = food.productQuantity!!
|
||||
} else if (food.servingSize != null || food.quantity != null) {
|
||||
eaten = (food.servingSize ?: food.quantity)!!.trim().replace(
|
||||
Regex(" +\\w+$"),
|
||||
""
|
||||
).toInt().toFloat()
|
||||
}
|
||||
}
|
||||
Log.d("Food", "$food")
|
||||
return Food(
|
||||
name = food.name!!,
|
||||
// do some slight edit on the serving to remove strange entries like `100 g`
|
||||
serving = (food.servingSize ?: food.quantity ?: "unknown").replace(Regex(" +"), ""),
|
||||
quantity = eaten,
|
||||
proteins = food.nutriments!!.proteins,
|
||||
carbohydrates = food.nutriments!!.carbohydrates,
|
||||
fat = food.nutriments!!.fat,
|
||||
// handle case where the energy is not given in kcal but only in kj
|
||||
energy = food.nutriments!!.energy
|
||||
?: (food.nutriments!!.energyKJ * 0.2390057361).toFloat(),
|
||||
image = food.image
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
21
app/src/main/java/com/dzeio/openhealth/data/food/FoodDao.kt
Normal file
21
app/src/main/java/com/dzeio/openhealth/data/food/FoodDao.kt
Normal file
@ -0,0 +1,21 @@
|
||||
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,84 @@
|
||||
package com.dzeio.openhealth.data.food
|
||||
|
||||
import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService
|
||||
import com.dzeio.openhealth.utils.NetworkResult
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FoodRepository @Inject constructor(
|
||||
private val dao: FoodDao,
|
||||
private val offSource: OpenFoodFactService
|
||||
) {
|
||||
|
||||
suspend fun searchFood(name: String): Flow<NetworkResult<List<Food>>> = channelFlow {
|
||||
val result = NetworkResult<List<Food>>()
|
||||
val items = arrayListOf<Food>()
|
||||
var otherFinished = false
|
||||
|
||||
launch { // Search OFF
|
||||
try {
|
||||
val request = offSource.searchProducts(name)
|
||||
if (!request.isSuccessful) {
|
||||
if (otherFinished) {
|
||||
result.status = NetworkResult.NetworkStatus.ERRORED
|
||||
} else {
|
||||
otherFinished = true
|
||||
}
|
||||
send(result)
|
||||
return@launch
|
||||
}
|
||||
val offProducts =
|
||||
offSource.searchProducts(name)
|
||||
.body()?.products?.map { Food.fromOpenFoodFact(it) }
|
||||
|
||||
if (offProducts != null) {
|
||||
items.addAll(offProducts.filterNotNull())
|
||||
result.data = items
|
||||
if (otherFinished) {
|
||||
result.status = NetworkResult.NetworkStatus.FINISHED
|
||||
} else {
|
||||
otherFinished = true
|
||||
}
|
||||
send(result)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
if (otherFinished) {
|
||||
result.status = NetworkResult.NetworkStatus.ERRORED
|
||||
} else {
|
||||
otherFinished = true
|
||||
}
|
||||
send(result)
|
||||
}
|
||||
}
|
||||
|
||||
launch { // search local DB
|
||||
getAll().collectLatest { list ->
|
||||
val filtered = list.filter { it.name.contains(name, true) }
|
||||
items.removeAll { it.id > 0 }
|
||||
items.addAll(0, filtered)
|
||||
|
||||
result.data = items
|
||||
if (otherFinished) {
|
||||
result.status = NetworkResult.NetworkStatus.FINISHED
|
||||
} else {
|
||||
otherFinished = true
|
||||
}
|
||||
send(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAll() = dao.getAll()
|
||||
|
||||
suspend fun add(food: Food) = dao.insert(food)
|
||||
|
||||
fun getById(id: Long) = dao.getOne(id)
|
||||
|
||||
suspend fun delete(food: Food) = dao.delete(food)
|
||||
|
||||
suspend fun update(food: Food) = dao.update(food)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.dzeio.openhealth.data.openfoodfact
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class OFFNutriments(
|
||||
/**
|
||||
* the quantity of carbohydrates in a 100g serving
|
||||
*/
|
||||
@SerializedName("carbohydrates_100g")
|
||||
var carbohydrates: Float,
|
||||
|
||||
/**
|
||||
* the energy in kcal in a 100g serving
|
||||
*/
|
||||
@SerializedName("energy-kcal_100g")
|
||||
var energy: Float?,
|
||||
|
||||
/**
|
||||
* the energy KL in a 100g serving
|
||||
*/
|
||||
@SerializedName("energy-kj_100g")
|
||||
var energyKJ: Float,
|
||||
|
||||
/**
|
||||
* the quantity of fat in a 100g serving
|
||||
*/
|
||||
@SerializedName("fat_100g")
|
||||
var fat: Float,
|
||||
|
||||
/**
|
||||
* the quantity of proteins in a 100g serving
|
||||
*/
|
||||
@SerializedName("proteins_100g")
|
||||
var proteins: Float
|
||||
)
|
@ -0,0 +1,53 @@
|
||||
package com.dzeio.openhealth.data.openfoodfact
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class OFFProduct(
|
||||
/**
|
||||
* the OFF product id
|
||||
*/
|
||||
@SerializedName("_id")
|
||||
var id: String,
|
||||
|
||||
/**
|
||||
* the product name
|
||||
*/
|
||||
@SerializedName("product_name")
|
||||
var name: String?,
|
||||
|
||||
/**
|
||||
* the size of a serving
|
||||
*/
|
||||
@SerializedName("serving_size")
|
||||
var servingSize: String?,
|
||||
|
||||
/**
|
||||
* the size of a serving without the `g`, `ml`, etc
|
||||
*/
|
||||
@SerializedName("serving_quantity")
|
||||
var servingQuantity: Float?,
|
||||
|
||||
/**
|
||||
* the size of a serving
|
||||
*/
|
||||
@SerializedName("quantity")
|
||||
var quantity: String?,
|
||||
|
||||
/**
|
||||
* the size of a serving without the `g`, `ml`, etc
|
||||
*/
|
||||
@SerializedName("product_quantity")
|
||||
var productQuantity: Float?,
|
||||
|
||||
/**
|
||||
* the product nutriments
|
||||
*/
|
||||
@SerializedName("nutriments")
|
||||
var nutriments: OFFNutriments?,
|
||||
|
||||
/**
|
||||
* the product image
|
||||
*/
|
||||
@SerializedName("image_url")
|
||||
var image: String?
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
package com.dzeio.openhealth.data.openfoodfact
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class OFFResult(
|
||||
/**
|
||||
* the list of products
|
||||
*/
|
||||
@SerializedName("products")
|
||||
var products: List<OFFProduct>
|
||||
)
|
@ -0,0 +1,53 @@
|
||||
package com.dzeio.openhealth.data.openfoodfact
|
||||
|
||||
import com.dzeio.openhealth.BuildConfig
|
||||
import com.google.gson.GsonBuilder
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a product by it's name
|
||||
*/
|
||||
@Headers(
|
||||
"User-Agent: OpenHealth - Android - Version ${BuildConfig.VERSION_NAME} - https://github.com/dzeiocom/OpenHealth"
|
||||
)
|
||||
@GET(
|
||||
"/cgi/search.pl?json=true&fields=_id,nutriments,product_name,serving_quantity,serving_size,quantity,product_quantity,image_url&action=process"
|
||||
)
|
||||
suspend fun searchProducts(@Query("search_terms2") name: String): Response<OFFResult>
|
||||
|
||||
/**
|
||||
* Search a product by it's barcode
|
||||
*/
|
||||
@Headers(
|
||||
"User-Agent: OpenHealth - Android - Version ${BuildConfig.VERSION_NAME} - https://github.com/dzeiocom/OpenHealth"
|
||||
)
|
||||
@GET("/api/v2/search?fields=_id,nutriments,product_name,serving_quantity")
|
||||
suspend fun findByCode(@Query("code") code: String): Response<OFFResult>
|
||||
}
|
@ -12,6 +12,10 @@ import java.util.TimeZone
|
||||
@Entity()
|
||||
data class Step(
|
||||
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||
|
||||
/**
|
||||
* the raw number of step
|
||||
*/
|
||||
var value: Int = 0,
|
||||
/**
|
||||
* Timestamp down to an hour
|
||||
@ -20,6 +24,12 @@ data class Step(
|
||||
*/
|
||||
@ColumnInfo(index = true)
|
||||
var timestamp: Long = 0,
|
||||
|
||||
/**
|
||||
* the source for the Entry
|
||||
*
|
||||
* note: Unused currently but kept for future usage
|
||||
*/
|
||||
var source: String = "OpenHealth"
|
||||
) {
|
||||
|
||||
@ -34,19 +44,24 @@ data class Step(
|
||||
}
|
||||
}
|
||||
|
||||
fun formatTimestamp(): String {
|
||||
val formatter = DateFormat.getDateTimeInstance(
|
||||
DateFormat.SHORT,
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
fun formatTimestamp(removeTime: Boolean = false): String {
|
||||
val formatter = if (removeTime) {
|
||||
DateFormat.getDateInstance(
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
} else {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.SHORT,
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
}
|
||||
return formatter.format(Date(this.timestamp))
|
||||
}
|
||||
|
||||
fun isToday(): Boolean {
|
||||
val it = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
it.timeInMillis = timestamp
|
||||
it.set(Calendar.HOUR, 0)
|
||||
val it = getDay()
|
||||
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
|
||||
@ -54,7 +69,17 @@ data class Step(
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
return it.timeInMillis == cal.timeInMillis
|
||||
return it == cal.timeInMillis
|
||||
}
|
||||
|
||||
fun getDay(): Long {
|
||||
val it = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
|
||||
timeInMillis = timestamp
|
||||
set(Calendar.HOUR, 0)
|
||||
|
||||
set(Calendar.AM_PM, Calendar.AM)
|
||||
}
|
||||
return it.timeInMillis
|
||||
}
|
||||
|
||||
fun isCurrent(): Boolean {
|
||||
|
@ -11,13 +11,16 @@ interface StepDao : BaseDao<Step> {
|
||||
@Query("SELECT * FROM Step ORDER BY timestamp DESC")
|
||||
fun getAll(): Flow<List<Step>>
|
||||
|
||||
@Query("SELECT * FROM Step where id = :weightId")
|
||||
@Query("SELECT * FROM Step WHERE timestamp >= :time")
|
||||
fun getAfter(time: Long): Flow<List<Step>>
|
||||
|
||||
@Query("SELECT * FROM Step WHERE id = :weightId")
|
||||
fun getOne(weightId: Long): Flow<Step?>
|
||||
|
||||
@Query("Select count(*) from Step")
|
||||
@Query("SELECT count(*) FROM Step")
|
||||
fun getCount(): Flow<Int>
|
||||
|
||||
@Query("Select * FROM Step ORDER BY timestamp DESC LIMIT 1")
|
||||
@Query("SELECT * FROM Step ORDER BY timestamp DESC LIMIT 1")
|
||||
fun last(): Flow<Step?>
|
||||
|
||||
@Query("DELETE FROM Step where source = :source")
|
||||
|
@ -1,10 +1,11 @@
|
||||
package com.dzeio.openhealth.data.step
|
||||
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
@Singleton
|
||||
class StepRepository @Inject constructor(
|
||||
@ -23,7 +24,13 @@ class StepRepository @Inject constructor(
|
||||
suspend fun deleteFromSource(value: String) = stepDao.deleteFromSource(value)
|
||||
|
||||
suspend fun todaySteps(): Int {
|
||||
val steps = getSteps().firstOrNull()
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
val steps = stepDao.getAfter(cal.timeInMillis).firstOrNull()
|
||||
if (steps == null) {
|
||||
return 0
|
||||
}
|
||||
@ -39,5 +46,4 @@ class StepRepository @Inject constructor(
|
||||
fun currentStep() = lastStep().filter {
|
||||
return@filter it != null && it.isCurrent()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,15 +7,21 @@ import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.dzeio.openhealth.Application
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Class that allows us to get Sensor datas for the internal step counter
|
||||
*
|
||||
* TODO: rewrite to use the new libs
|
||||
*/
|
||||
class StepSource(
|
||||
private val context: Context,
|
||||
context: Context,
|
||||
private val callback: ((Float) -> Unit)? = null
|
||||
): SensorEventListener {
|
||||
) : SensorEventListener {
|
||||
|
||||
companion object {
|
||||
const val TAG = "${Application.TAG}/StepSource"
|
||||
@ -28,27 +34,31 @@ class StepSource(
|
||||
return prefs.getLong("steps_time_since_last_record", Long.MAX_VALUE)
|
||||
}
|
||||
set(value) {
|
||||
val editor = prefs.edit()
|
||||
editor.putLong("steps_time_since_last_record", value)
|
||||
editor.commit()
|
||||
prefs.edit {
|
||||
putLong("steps_time_since_last_record", value)
|
||||
}
|
||||
}
|
||||
private var stepsAsOfLastRecord: Float
|
||||
get() {
|
||||
return prefs.getFloat("steps_as_of_last_record", 0f)
|
||||
}
|
||||
set(value) {
|
||||
val editor = prefs.edit()
|
||||
editor.putFloat("steps_as_of_last_record", value)
|
||||
editor.commit()
|
||||
prefs.edit {
|
||||
putFloat("steps_as_of_last_record", value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
Log.d(TAG, "Setting up")
|
||||
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
val stepCountSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
|
||||
stepCountSensor.let {
|
||||
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
sensorManager.registerListener(
|
||||
this,
|
||||
it,
|
||||
SensorManager.SENSOR_DELAY_NORMAL,
|
||||
SensorManager.SENSOR_DELAY_NORMAL
|
||||
)
|
||||
Log.d(TAG, "should be setup :D")
|
||||
}
|
||||
}
|
||||
@ -68,7 +78,10 @@ class StepSource(
|
||||
|
||||
// don't send changes since it wasn't made when the app was running
|
||||
if (timeSinceLastBoot < timeSinceLastRecord) {
|
||||
Log.d(TAG, "Skipping since we don't know when many steps are taken since last boot ($timeSinceLastRecord, $timeSinceLastBoot)")
|
||||
Log.d(
|
||||
TAG,
|
||||
"Skipping since we don't know when many steps are taken since last boot ($timeSinceLastRecord, $timeSinceLastBoot)"
|
||||
)
|
||||
timeSinceLastRecord = timeSinceLastBoot
|
||||
return@let
|
||||
}
|
||||
|
@ -11,9 +11,23 @@ import java.util.TimeZone
|
||||
@Entity()
|
||||
data class Water(
|
||||
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||
|
||||
/**
|
||||
* the quantity of water in ML drank by the user
|
||||
*/
|
||||
var value: Int = 0,
|
||||
|
||||
/**
|
||||
* when the water was drank precise to the day
|
||||
*/
|
||||
@ColumnInfo(index = true)
|
||||
var timestamp: Long = 0,
|
||||
|
||||
/**
|
||||
* the source for the Entry
|
||||
*
|
||||
* note: Unused currently but kept for future usage
|
||||
*/
|
||||
var source: String = "OpenHealth"
|
||||
) {
|
||||
init {
|
||||
|
@ -1,10 +1,8 @@
|
||||
package com.dzeio.openhealth.data.water
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
@Singleton
|
||||
class WaterRepository @Inject constructor(
|
||||
@ -23,4 +21,4 @@ class WaterRepository @Inject constructor(
|
||||
fun todayWater() = lastWater().filter {
|
||||
return@filter it != null && it.isToday()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,77 @@ data class Weight(
|
||||
* Store the weight in kilograms
|
||||
*/
|
||||
var weight: Float = 0f,
|
||||
|
||||
/**
|
||||
* when the weight was taken precise to the millisecond
|
||||
*/
|
||||
@ColumnInfo(index = true)
|
||||
var timestamp: Long = System.currentTimeMillis(),
|
||||
var source: String = ""
|
||||
|
||||
/**
|
||||
* the source for the Entry
|
||||
*
|
||||
* note: Unused currently but kept for future usage
|
||||
*/
|
||||
var source: String = "OpenHealth",
|
||||
|
||||
/**
|
||||
* The BMI
|
||||
*
|
||||
* calculated from the height and Weight of the user
|
||||
*
|
||||
* ex: weight / (height(cm)/100)²
|
||||
*/
|
||||
var bmi: Float? = null,
|
||||
|
||||
/**
|
||||
* the total body water (tbw) in percents
|
||||
*
|
||||
* can be estimated
|
||||
* https://www.mdapp.co/total-body-water-tbw-calculator-448/
|
||||
*/
|
||||
var totalBodyWater: Float? = null,
|
||||
|
||||
/**
|
||||
* the Muscle weight in percents
|
||||
*/
|
||||
var muscles: Float? = null,
|
||||
|
||||
/**
|
||||
* the lean Body Mass in percents
|
||||
*/
|
||||
var leanBodyMass: Float? = null,
|
||||
|
||||
/**
|
||||
* the Body Fat in percents
|
||||
*/
|
||||
var bodyFat: Float? = null,
|
||||
|
||||
/**
|
||||
* the bone Mass in Percents
|
||||
*/
|
||||
var boneMass: Float? = null,
|
||||
|
||||
/**
|
||||
* visceral fat in it's own unit?
|
||||
*/
|
||||
var visceralFat: Float? = null
|
||||
) {
|
||||
fun formatTimestamp(): String = getDateInstance().format(Date(timestamp));
|
||||
fun formatTimestamp(): String = getDateInstance().format(Date(timestamp))
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (!(other is Weight)) {
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
return weight == other.weight &&
|
||||
timestamp == other.timestamp &&
|
||||
bmi == other.bmi &&
|
||||
totalBodyWater == other.totalBodyWater &&
|
||||
muscles == other.muscles &&
|
||||
leanBodyMass == other.leanBodyMass &&
|
||||
bodyFat == other.bodyFat &&
|
||||
boneMass == other.boneMass &&
|
||||
visceralFat == other.visceralFat
|
||||
}
|
||||
}
|
||||
|
@ -22,4 +22,4 @@ interface WeightDao : BaseDao<Weight> {
|
||||
|
||||
@Query("DELETE FROM Weight where source = :source")
|
||||
suspend fun deleteFromSource(source: String)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ class WeightRepository @Inject constructor(
|
||||
fun getWeight(id: Long) = weightDao.getOne(id)
|
||||
|
||||
suspend fun addWeight(weight: Weight) = weightDao.insert(weight)
|
||||
suspend fun deleteWeight(weight: Weight) = weightDao.delete(weight)
|
||||
suspend fun addAll(vararg weight: Weight) = weightDao.insertAll(*weight)
|
||||
suspend fun deleteWeight(vararg weight: Weight) = weightDao.delete(*weight)
|
||||
suspend fun deleteFromSource(source: String) = weightDao.deleteFromSource(source)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,292 @@
|
||||
package com.dzeio.openhealth.devices
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.BluetoothGattService
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.dzeio.openhealth.core.Observable
|
||||
import com.dzeio.openhealth.utils.Bluetooth
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import com.dzeio.openhealth.utils.polyfills.writeCharacteristicPoly
|
||||
import com.dzeio.openhealth.utils.polyfills.writeDescriptorPoly
|
||||
import java.util.UUID
|
||||
|
||||
abstract class BluetoothLeGattDevice(
|
||||
var bluetooth: Bluetooth,
|
||||
config: Configuration
|
||||
) : Device<BluetoothDevice>(config) {
|
||||
|
||||
private val status = Observable(ConnectionStatus.DISCONNECTED)
|
||||
|
||||
companion object {
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_SCAN")
|
||||
fun findDevices(
|
||||
bluetooth: Bluetooth,
|
||||
config: Configuration
|
||||
): Observable<ArrayList<BluetoothLeGattDevice>?> {
|
||||
val list = arrayListOf<BluetoothLeGattDevice>()
|
||||
val obs = Observable<ArrayList<BluetoothLeGattDevice>?>(null)
|
||||
val devices = DeviceFactory.getBluetoothLEDevices(bluetooth, config)
|
||||
|
||||
bluetooth.scanLeDevices {
|
||||
if (list.find { item -> item.item?.address == it.address } != null) {
|
||||
return@scanLeDevices false
|
||||
}
|
||||
for (device in devices) {
|
||||
if (device.isOfType(it)) {
|
||||
device.item = it
|
||||
list.add(device)
|
||||
obs.value = list
|
||||
}
|
||||
}
|
||||
return@scanLeDevices false
|
||||
}
|
||||
return obs
|
||||
}
|
||||
}
|
||||
|
||||
protected val services = arrayListOf<BluetoothGattService>()
|
||||
protected lateinit var gatt: BluetoothGatt
|
||||
|
||||
private val gattCallback = ConnectionCallback()
|
||||
|
||||
override fun search(): Observable<BluetoothDevice?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun connect(): Observable<ConnectionStatus> {
|
||||
if (item == null) {
|
||||
status.value = ConnectionStatus.ERROR
|
||||
return status
|
||||
}
|
||||
status.value = ConnectionStatus.CONNECTING
|
||||
|
||||
bluetooth.connectGatt(item!!, true, gattCallback)
|
||||
return status
|
||||
}
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
|
||||
fun close() {
|
||||
gatt.close()
|
||||
}
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
|
||||
protected fun writeBytes(service: UUID, characteristic: UUID, value: ByteArray) {
|
||||
val t = findCharacteristic(service, characteristic)
|
||||
if (t == null) {
|
||||
Log.e("BluetoothLeDevice", "Could not write characteristic")
|
||||
return
|
||||
}
|
||||
|
||||
gatt.writeCharacteristicPoly(
|
||||
t,
|
||||
value,
|
||||
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
|
||||
protected fun setNotification(service: UUID, characteristic: UUID, value: Boolean): Boolean {
|
||||
val char = findCharacteristic(service, characteristic)
|
||||
if (char == null) {
|
||||
Log.e("BluetoothLeDevice", "Could not set notification on characteristic")
|
||||
return false
|
||||
}
|
||||
gatt.setCharacteristicNotification(
|
||||
char,
|
||||
value
|
||||
)
|
||||
gatt.writeDescriptorPoly(
|
||||
char.getDescriptor(
|
||||
BluetoothLeGattUuid.DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION
|
||||
),
|
||||
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun findCharacteristic(
|
||||
service: UUID,
|
||||
characteristic: UUID
|
||||
): BluetoothGattCharacteristic? {
|
||||
return gatt.services.find { it.uuid == service }?.getCharacteristic(characteristic)
|
||||
}
|
||||
|
||||
open fun onPhyUpdate(gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, status: Int) {}
|
||||
|
||||
open fun onPhyRead(gatt: BluetoothGatt?, txPhy: Int, rxPhy: Int, status: Int) {}
|
||||
|
||||
open fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {}
|
||||
|
||||
open fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {}
|
||||
|
||||
open fun onCharacteristicRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray,
|
||||
status: Int
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onCharacteristicWrite(
|
||||
gatt: BluetoothGatt?,
|
||||
characteristic: BluetoothGattCharacteristic?,
|
||||
status: Int
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onDescriptorRead(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
status: Int,
|
||||
value: ByteArray
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onDescriptorWrite(
|
||||
gatt: BluetoothGatt?,
|
||||
descriptor: BluetoothGattDescriptor?,
|
||||
status: Int
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onReliableWriteCompleted(gatt: BluetoothGatt?, status: Int) {}
|
||||
|
||||
open fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) {}
|
||||
|
||||
open fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {}
|
||||
|
||||
open fun onServiceChanged(gatt: BluetoothGatt) {}
|
||||
|
||||
private inner class ConnectionCallback : BluetoothGattCallback() {
|
||||
override fun onPhyUpdate(
|
||||
gatt: BluetoothGatt?,
|
||||
txPhy: Int,
|
||||
rxPhy: Int,
|
||||
status: Int
|
||||
) {
|
||||
Log.d("onPhyUpdate", "$gatt, $txPhy, $rxPhy, $status")
|
||||
this@BluetoothLeGattDevice.onPhyUpdate(gatt, txPhy, rxPhy, status)
|
||||
}
|
||||
|
||||
override fun onPhyRead(
|
||||
gatt: BluetoothGatt?,
|
||||
txPhy: Int,
|
||||
rxPhy: Int,
|
||||
status: Int
|
||||
) {
|
||||
Log.d("onPhyRead", "$gatt, $txPhy, $rxPhy, $status")
|
||||
this@BluetoothLeGattDevice.onPhyRead(gatt, txPhy, rxPhy, status)
|
||||
}
|
||||
|
||||
@RequiresPermission("android.permission.BLUETOOTH_CONNECT")
|
||||
override fun onConnectionStateChange(
|
||||
gatt: BluetoothGatt?,
|
||||
status: Int,
|
||||
newState: Int
|
||||
) {
|
||||
Log.d("onConnectionStateChange", "$gatt, $status, $newState")
|
||||
if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
this@BluetoothLeGattDevice.gatt = gatt
|
||||
gatt.discoverServices()
|
||||
}
|
||||
this@BluetoothLeGattDevice.onConnectionStateChange(gatt, status, newState)
|
||||
}
|
||||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
||||
Log.d("onServicesDiscovered", "$gatt, $status")
|
||||
if (gatt != null) {
|
||||
this@BluetoothLeGattDevice.status.value = ConnectionStatus.CONNECTED
|
||||
services.addAll(gatt.services)
|
||||
}
|
||||
this@BluetoothLeGattDevice.onServicesDiscovered(gatt, status)
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray,
|
||||
status: Int
|
||||
) {
|
||||
Log.d("onCharacteristicRead", "$gatt, $characteristic, $status, $value")
|
||||
this@BluetoothLeGattDevice.onCharacteristicRead(gatt, characteristic, value, status)
|
||||
}
|
||||
|
||||
override fun onCharacteristicWrite(
|
||||
gatt: BluetoothGatt?,
|
||||
characteristic: BluetoothGattCharacteristic?,
|
||||
status: Int
|
||||
) {
|
||||
Log.d("onCharacteristicWrite", "$gatt, $characteristic, $status")
|
||||
this@BluetoothLeGattDevice.onCharacteristicWrite(gatt, characteristic, status)
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
) {
|
||||
Log.d("onCharacteristicChanged", "$gatt, $characteristic, ${byteArrayToHex(value)}")
|
||||
this@BluetoothLeGattDevice.onCharacteristicChanged(gatt, characteristic, value)
|
||||
}
|
||||
|
||||
override fun onDescriptorRead(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
status: Int,
|
||||
value: ByteArray
|
||||
) {
|
||||
Log.d("onDescriptorRead", "$gatt, $descriptor, $status, $value")
|
||||
this@BluetoothLeGattDevice.onDescriptorRead(gatt, descriptor, status, value)
|
||||
}
|
||||
|
||||
override fun onDescriptorWrite(
|
||||
gatt: BluetoothGatt?,
|
||||
descriptor: BluetoothGattDescriptor?,
|
||||
status: Int
|
||||
) {
|
||||
Log.d("onDescriptorWrite", "$gatt, $descriptor, $status")
|
||||
this@BluetoothLeGattDevice.onDescriptorWrite(gatt, descriptor, status)
|
||||
}
|
||||
|
||||
override fun onReliableWriteCompleted(gatt: BluetoothGatt?, status: Int) {
|
||||
Log.d("onReliableWriteComplete", "$gatt, $status")
|
||||
this@BluetoothLeGattDevice.onReliableWriteCompleted(gatt, status)
|
||||
}
|
||||
|
||||
override fun onReadRemoteRssi(
|
||||
gatt: BluetoothGatt?,
|
||||
rssi: Int,
|
||||
status: Int
|
||||
) {
|
||||
Log.d("onReadRemoteRssi", "$gatt, $rssi, $status")
|
||||
this@BluetoothLeGattDevice.onReadRemoteRssi(gatt, rssi, status)
|
||||
}
|
||||
|
||||
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
|
||||
Log.d("onMtuChanged", "$gatt, $mtu, $status")
|
||||
this@BluetoothLeGattDevice.onMtuChanged(gatt, mtu, status)
|
||||
}
|
||||
|
||||
override fun onServiceChanged(gatt: BluetoothGatt) {
|
||||
Log.d("onServiceChanged", "$gatt")
|
||||
this@BluetoothLeGattDevice.onServiceChanged(gatt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun byteArrayToHex(arr: ByteArray): String =
|
||||
arr.joinToString(" ") { String.format("%02X", it) }
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.dzeio.openhealth.devices
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
object BluetoothLeGattUuid {
|
||||
private const val STANDARD_SUFFIX = "-0000-1000-8000-00805f9b34fb"
|
||||
fun fromShortCode(code: Long): UUID {
|
||||
return UUID.fromString(String.format("%08x%s", code, STANDARD_SUFFIX))
|
||||
}
|
||||
|
||||
// https://www.bluetooth.com/specifications/gatt/services
|
||||
val SERVICE_GENERIC_ACCESS = fromShortCode(0x1800) // 1800
|
||||
val SERVICE_GENERIC_ATTRIBUTE = fromShortCode(0x1801) // 1801
|
||||
|
||||
val SERVICE_CURRENT_TIME = fromShortCode(0x1805)
|
||||
val SERVICE_DEVICE_INFORMATION = fromShortCode(0x180A)
|
||||
val SERVICE_BATTERY_LEVEL = fromShortCode(0x180F)
|
||||
val SERVICE_BODY_COMPOSITION = fromShortCode(0x181B) // 181B
|
||||
val SERVICE_USER_DATA = fromShortCode(0x181C)
|
||||
val SERVICE_WEIGHT_SCALE = fromShortCode(0x181D)
|
||||
|
||||
// https://www.bluetooth.com/specifications/gatt/characteristics
|
||||
val CHARACTERISTIC_DEVICE_NAME = fromShortCode(0x2A00)
|
||||
val CHARACTERISTIC_APPEARANCE = fromShortCode(0x2A01)
|
||||
val CHARACTERISTIC_PERIPHERAL_PRIVACY_FLAG = fromShortCode(0x2A02)
|
||||
val CHARACTERISTIC_RECONNECTION_ADDRESS = fromShortCode(0x2A03)
|
||||
val CHARACTERISTIC_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS = fromShortCode(0x2A04)
|
||||
val CHARACTERISTIC_SERVICE_CHANGED = fromShortCode(0x2A05)
|
||||
|
||||
val CHARACTERISTIC_BATTERY_LEVEL = fromShortCode(0x2A19)
|
||||
|
||||
val CHARACTERISTIC_SYSTEM_ID = fromShortCode(0x2A23)
|
||||
val CHARACTERISTIC_MODEL_NUMBER_STRING = fromShortCode(0x2A24)
|
||||
val CHARACTERISTIC_SERIAL_NUMBER_STRING = fromShortCode(0x2A25)
|
||||
val CHARACTERISTIC_FIRMWARE_REVISION_STRING = fromShortCode(0x2A26)
|
||||
val CHARACTERISTIC_HARDWARE_REVISION_STRING = fromShortCode(0x2A27)
|
||||
val CHARACTERISTIC_SOFTWARE_REVISION_STRING = fromShortCode(0x2A28)
|
||||
val CHARACTERISTIC_MANUFACTURER_NAME_STRING = fromShortCode(0x2A29)
|
||||
val CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST = fromShortCode(0x2A2A)
|
||||
val CHARACTERISTIC_CURRENT_TIME = fromShortCode(0x2A2B)
|
||||
|
||||
val CHARACTERISTIC_PNP_ID = fromShortCode(0x2A50)
|
||||
|
||||
val CHARACTERISTIC_USER_AGE = fromShortCode(0x2A80)
|
||||
val CHARACTERISTIC_USER_DATE_OF_BIRTH = fromShortCode(0x2A85)
|
||||
val CHARACTERISTIC_USER_GENDER = fromShortCode(0x2A8C)
|
||||
val CHARACTERISTIC_USER_HEIGHT = fromShortCode(0x2A8E)
|
||||
|
||||
val CHARACTERISTIC_CHANGE_INCREMENT = fromShortCode(0x2A99)
|
||||
val CHARACTERISTIC_BODY_COMPOSITION_MEASUREMENT = fromShortCode(0x2A9C)
|
||||
val CHARACTERISTIC_WEIGHT_MEASUREMENT = fromShortCode(0x2A9D)
|
||||
val CHARACTERISTIC_USER_CONTROL_POINT = fromShortCode(0x2A9F)
|
||||
|
||||
// https://www.bluetooth.com/specifications/gatt/descriptors
|
||||
val DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = fromShortCode(0x2902)
|
||||
val DESCRIPTOR_CHARACTERISTIC_USER_DESCRIPTION = fromShortCode(0x2901)
|
||||
}
|
38
app/src/main/java/com/dzeio/openhealth/devices/Device.kt
Normal file
38
app/src/main/java/com/dzeio/openhealth/devices/Device.kt
Normal file
@ -0,0 +1,38 @@
|
||||
package com.dzeio.openhealth.devices
|
||||
|
||||
import com.dzeio.openhealth.core.Observable
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
|
||||
abstract class Device<T>(
|
||||
val config: Configuration
|
||||
) {
|
||||
|
||||
abstract val name: String
|
||||
var item: T? = null
|
||||
|
||||
enum class ConnectionStatus {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
data class FetchStatus(
|
||||
var progress: Int,
|
||||
var progressMax: Int,
|
||||
val data: ArrayList<Weight> = arrayListOf()
|
||||
)
|
||||
|
||||
// enum class ActionStatus {}
|
||||
|
||||
abstract fun isOfType(item: T): Boolean
|
||||
|
||||
abstract fun search(): Observable<T?>
|
||||
|
||||
abstract fun connect(): Observable<ConnectionStatus>
|
||||
|
||||
abstract fun fetchWeights(): Observable<FetchStatus>
|
||||
|
||||
abstract fun reset()
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.dzeio.openhealth.devices
|
||||
|
||||
import com.dzeio.openhealth.utils.Bluetooth
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
|
||||
object DeviceFactory {
|
||||
|
||||
fun getBluetoothLEDevices(
|
||||
bluetooth: Bluetooth,
|
||||
configuration: Configuration
|
||||
): ArrayList<BluetoothLeGattDevice> {
|
||||
return arrayListOf(
|
||||
DeviceMiSmartScale2(bluetooth, configuration)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,297 @@
|
||||
package com.dzeio.openhealth.devices
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.dzeio.openhealth.core.Observable
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.devices.libs.MiScaleLib
|
||||
import com.dzeio.openhealth.utils.Bluetooth
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Random
|
||||
import java.util.UUID
|
||||
|
||||
class DeviceMiSmartScale2(
|
||||
bluetooth: Bluetooth,
|
||||
config: Configuration
|
||||
) : BluetoothLeGattDevice(bluetooth, config) {
|
||||
|
||||
companion object {
|
||||
val TAG = this::class.java.name
|
||||
}
|
||||
|
||||
override val name = "Mi Smart Scale 2"
|
||||
|
||||
private val WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC =
|
||||
UUID.fromString("00002a2f-0000-3512-2118-0009af100700")
|
||||
private val WEIGHT_CUSTOM_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700")
|
||||
private val WEIGHT_CUSTOM_CONFIG = UUID.fromString("00001542-0000-3512-2118-0009af100700")
|
||||
|
||||
private val CONFIG_USER_ID = "com.dzeio.open-health.devices.mi-scale-2.id"
|
||||
|
||||
private val fetchStatus = Observable(FetchStatus(0, 5))
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun isOfType(item: BluetoothDevice): Boolean =
|
||||
item.name == "MIBFS"
|
||||
|
||||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
override fun fetchWeights(): Observable<FetchStatus> {
|
||||
fetchStatus.value = FetchStatus(0, 5)
|
||||
// step 0
|
||||
writeBytes(
|
||||
WEIGHT_CUSTOM_SERVICE,
|
||||
WEIGHT_CUSTOM_CONFIG,
|
||||
byteArrayOf(
|
||||
0x06,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00
|
||||
)
|
||||
)
|
||||
|
||||
return fetchStatus
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
override fun onCharacteristicWrite(
|
||||
gatt: BluetoothGatt?,
|
||||
characteristic: BluetoothGattCharacteristic?,
|
||||
status: Int
|
||||
) {
|
||||
if (characteristic?.uuid == WEIGHT_CUSTOM_CONFIG) {
|
||||
// step 1
|
||||
// set current time
|
||||
val currentDateTime: Calendar = Calendar.getInstance()
|
||||
val year: Int = currentDateTime.get(Calendar.YEAR)
|
||||
val month = (currentDateTime.get(Calendar.MONTH) + 1)
|
||||
val day = currentDateTime.get(Calendar.DAY_OF_MONTH)
|
||||
val hour = currentDateTime.get(Calendar.HOUR_OF_DAY)
|
||||
val min = currentDateTime.get(Calendar.MINUTE)
|
||||
val sec = currentDateTime.get(Calendar.SECOND)
|
||||
|
||||
val dateTimeByte = byteArrayOf(
|
||||
year.toByte(),
|
||||
(year shr 8).toByte(),
|
||||
month.toByte(),
|
||||
day.toByte(),
|
||||
hour.toByte(),
|
||||
min.toByte(),
|
||||
sec.toByte(),
|
||||
0x03,
|
||||
0x00,
|
||||
0x00
|
||||
)
|
||||
|
||||
writeBytes(
|
||||
BluetoothLeGattUuid.SERVICE_BODY_COMPOSITION,
|
||||
BluetoothLeGattUuid.CHARACTERISTIC_CURRENT_TIME,
|
||||
dateTimeByte
|
||||
)
|
||||
fetchStatus.value.progress++
|
||||
fetchStatus.notifyObservers()
|
||||
} else if (characteristic?.uuid == BluetoothLeGattUuid.CHARACTERISTIC_CURRENT_TIME) {
|
||||
// step 2
|
||||
setNotification(
|
||||
BluetoothLeGattUuid.SERVICE_BODY_COMPOSITION,
|
||||
WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC,
|
||||
true
|
||||
)
|
||||
fetchStatus.value.progress++
|
||||
fetchStatus.notifyObservers()
|
||||
} else if (
|
||||
fetchStatus.value.progress < 4 &&
|
||||
characteristic?.uuid == WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC
|
||||
) {
|
||||
// step 4
|
||||
writeBytes(
|
||||
BluetoothLeGattUuid.SERVICE_BODY_COMPOSITION,
|
||||
WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC,
|
||||
byteArrayOf(
|
||||
0x02
|
||||
)
|
||||
)
|
||||
fetchStatus.value.progress++
|
||||
fetchStatus.notifyObservers()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
override fun onDescriptorWrite(
|
||||
gatt: BluetoothGatt?,
|
||||
descriptor: BluetoothGattDescriptor?,
|
||||
status: Int
|
||||
) {
|
||||
// step 3
|
||||
super.onDescriptorWrite(gatt, descriptor, status)
|
||||
val id = getID()
|
||||
writeBytes(
|
||||
BluetoothLeGattUuid.SERVICE_BODY_COMPOSITION,
|
||||
WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC,
|
||||
byteArrayOf(
|
||||
0x01.toByte(),
|
||||
0xFF.toByte(),
|
||||
0xFF.toByte(),
|
||||
(id and 0xFF00 shl 8).toByte(),
|
||||
(id and 0x00FF shl 0).toByte()
|
||||
)
|
||||
)
|
||||
fetchStatus.value.progress++
|
||||
fetchStatus.notifyObservers()
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
) {
|
||||
super.onCharacteristicChanged(gatt, characteristic, value)
|
||||
|
||||
if (value.isNotEmpty()) {
|
||||
// step 5+x
|
||||
if (value[0] == 0x03.toByte()) {
|
||||
Log.d(TAG, "Stop signal received")
|
||||
writeBytes(
|
||||
BluetoothLeGattUuid.SERVICE_BODY_COMPOSITION,
|
||||
WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC,
|
||||
byteArrayOf(0x03)
|
||||
)
|
||||
|
||||
val id = getID()
|
||||
|
||||
val userIdentifier = byteArrayOf(
|
||||
0x04.toByte(),
|
||||
0xFF.toByte(),
|
||||
0xFF.toByte(),
|
||||
(id and 0xFF00 shr 8).toByte(),
|
||||
(id and 0xFF shr 0).toByte()
|
||||
)
|
||||
writeBytes(
|
||||
BluetoothLeGattUuid.SERVICE_BODY_COMPOSITION,
|
||||
WEIGHT_MEASUREMENT_HISTORY_CHARACTERISTIC,
|
||||
userIdentifier
|
||||
)
|
||||
fetchStatus.value.progress++
|
||||
fetchStatus.notifyObservers()
|
||||
} else if (value.size == 13) {
|
||||
// 4+x
|
||||
Log.d(TAG, "Measurement received")
|
||||
val weight = decodeEntry(value)
|
||||
if (weight == null) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchStatus.value.data.add(weight)
|
||||
fetchStatus.value.progress++
|
||||
fetchStatus.value.progressMax++
|
||||
fetchStatus.notifyObservers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
config.getInt(CONFIG_USER_ID).value = null
|
||||
}
|
||||
|
||||
private fun getID(): Int {
|
||||
val id = config.getInt(CONFIG_USER_ID)
|
||||
if (id.value == null) {
|
||||
id.value = (Random().nextInt(65535 - 100 + 1) + 100)
|
||||
}
|
||||
return id.value!! + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the 13 bytes entry into a [Weight] object
|
||||
*
|
||||
* control bytes
|
||||
* XX XX 00 00 00 00 00 00 00 00 00 00 00
|
||||
*
|
||||
* Datetime bytes
|
||||
* 00 00 XX XX XX XX XX XX XX 00 00 00 00
|
||||
*
|
||||
* Impedance Bytes
|
||||
* 00 00 00 00 00 00 00 00 00 XX XX 00 00
|
||||
*
|
||||
* Weight Bytes
|
||||
* 00 00 00 00 00 00 00 00 00 00 00 XX XX
|
||||
*/
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun decodeEntry(entry: ByteArray): Weight? {
|
||||
// byte 0
|
||||
val ctrlByte0 = entry[0]
|
||||
// byte 1
|
||||
val ctrlByte1 = entry[1]
|
||||
|
||||
val isLBSUnit = ctrlByte0.isBitSet(0)
|
||||
val isImpedance = ctrlByte1.isBitSet(1)
|
||||
val isStablilized = ctrlByte1.isBitSet(5)
|
||||
val isCattyUnit = ctrlByte1.isBitSet(6)
|
||||
val isWeightRemoved = ctrlByte1.isBitSet(7)
|
||||
|
||||
if (isWeightRemoved || !isStablilized) {
|
||||
return null
|
||||
}
|
||||
|
||||
// byte 2 to 8 represent the datetime
|
||||
val year = ((entry[3].toInt() and 0xFF) shl 8) or (entry[2].toInt() and 0xFF)
|
||||
val month = entry[4].toInt()
|
||||
val day = entry[5].toInt()
|
||||
val hours = entry[6].toInt()
|
||||
val min = entry[7].toInt()
|
||||
val sec = entry[8].toInt()
|
||||
|
||||
val weightTmp = ((entry[12].toInt() and 0xFF) shl 8) or (entry[11].toInt() and 0xFF)
|
||||
val weightFloat = if (isLBSUnit || isCattyUnit) weightTmp / 100f else weightTmp / 200f
|
||||
|
||||
val weight = Weight(
|
||||
weight = weightFloat
|
||||
)
|
||||
|
||||
var impedance: Float? = null
|
||||
if (isImpedance) {
|
||||
impedance =
|
||||
(((entry[10].toInt() and 0xFF) shl 8) or (entry[9].toInt() and 0xFF)).toFloat()
|
||||
}
|
||||
|
||||
val dateStr = "$year/$month/$day/$hours/$min/$sec"
|
||||
val date = SimpleDateFormat("yyyy/MM/dd/HH/mm/ss").parse(dateStr)?.time
|
||||
|
||||
if (date == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
weight.timestamp = date
|
||||
|
||||
if (impedance == null) {
|
||||
return weight
|
||||
}
|
||||
|
||||
val miScaleLib = MiScaleLib(1, 24, 179f)
|
||||
|
||||
weight.apply {
|
||||
bmi = miScaleLib.getBMI(weightFloat)
|
||||
totalBodyWater = miScaleLib.getWater(weightFloat, impedance)
|
||||
visceralFat = miScaleLib.getVisceralFat(weightFloat)
|
||||
bodyFat = miScaleLib.getBodyFat(weightFloat, impedance)
|
||||
muscles = miScaleLib.getMuscle(weightFloat, impedance) / weightFloat * 100
|
||||
leanBodyMass = miScaleLib.getLBM(weightFloat, impedance) / weightFloat * 100
|
||||
boneMass = miScaleLib.getBoneMass(weightFloat, impedance)
|
||||
}
|
||||
|
||||
return weight
|
||||
}
|
||||
|
||||
private fun Byte.isBitSet(pos: Int): Boolean {
|
||||
val nn = this.toInt() shr pos
|
||||
return (nn and 1) == 1
|
||||
}
|
||||
}
|
1
app/src/main/java/com/dzeio/openhealth/devices/README.md
Normal file
1
app/src/main/java/com/dzeio/openhealth/devices/README.md
Normal file
@ -0,0 +1 @@
|
||||
Most code from `./BluetoothGattUuid.kt`, `./DeviceMiSmartScale2.kt` and `./libs/MiScaleLib.java` was taken from [OpenScale](https://github.com/oliexdev/openScale)
|
@ -0,0 +1,175 @@
|
||||
/* Copyright (C) 2019 olie.xdev <olie.xdev@googlemail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
package com.dzeio.openhealth.devices.libs;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* based on <a href="https://github.com/prototux/MIBCS-reverse-engineering">...</a> by prototux
|
||||
* <p>
|
||||
* TODO: Carefully transform it into a Kotlin Class
|
||||
*/
|
||||
public class MiScaleLib {
|
||||
private final int sex; // male = 1; female = 0
|
||||
private final int age;
|
||||
private final float height;
|
||||
|
||||
public MiScaleLib(int sex, int age, float height) {
|
||||
this.sex = sex;
|
||||
this.age = age;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
private float getLBMCoefficient(float weight, float impedance) {
|
||||
float lbm = (height * 9.058f / 100.0f) * (height / 100.0f);
|
||||
lbm += weight * 0.32f + 12.226f;
|
||||
lbm -= impedance * 0.0068f;
|
||||
lbm -= age * 0.0542f;
|
||||
|
||||
return lbm;
|
||||
}
|
||||
|
||||
public float getBMI(float weight) {
|
||||
return weight / (((height * height) / 100.0f) / 100.0f);
|
||||
}
|
||||
|
||||
public float getLBM(float weight, float impedance) {
|
||||
float leanBodyMass = weight - ((getBodyFat(weight, impedance) * 0.01f) * weight) - getBoneMass(weight, impedance);
|
||||
|
||||
if (sex == 0 && leanBodyMass >= 84.0f) {
|
||||
leanBodyMass = 120.0f;
|
||||
}
|
||||
else if (sex == 1 && leanBodyMass >= 93.5f) {
|
||||
leanBodyMass = 120.0f;
|
||||
}
|
||||
|
||||
return leanBodyMass;
|
||||
}
|
||||
|
||||
public float getMuscle(float weight, float impedance) {
|
||||
return this.getLBM(weight,impedance); // this is wrong but coherent with MiFit app behaviour
|
||||
}
|
||||
|
||||
public float getWater(float weight, float impedance) {
|
||||
float coeff;
|
||||
float water = (100.0f - getBodyFat(weight, impedance)) * 0.7f;
|
||||
|
||||
if (water < 50) {
|
||||
coeff = 1.02f;
|
||||
} else {
|
||||
coeff = 0.98f;
|
||||
}
|
||||
|
||||
return coeff * water;
|
||||
}
|
||||
|
||||
public float getBoneMass(float weight, float impedance) {
|
||||
float boneMass;
|
||||
float base;
|
||||
|
||||
if (sex == 0) {
|
||||
base = 0.245691014f;
|
||||
}
|
||||
else {
|
||||
base = 0.18016894f;
|
||||
}
|
||||
|
||||
boneMass = (base - (getLBMCoefficient(weight, impedance) * 0.05158f)) * -1.0f;
|
||||
|
||||
if (boneMass > 2.2f) {
|
||||
boneMass += 0.1f;
|
||||
}
|
||||
else {
|
||||
boneMass -= 0.1f;
|
||||
}
|
||||
|
||||
if (sex == 0 && boneMass > 5.1f) {
|
||||
boneMass = 8.0f;
|
||||
}
|
||||
else if (sex == 1 && boneMass > 5.2f) {
|
||||
boneMass = 8.0f;
|
||||
}
|
||||
|
||||
return boneMass;
|
||||
}
|
||||
|
||||
public float getVisceralFat(float weight) {
|
||||
float visceralFat = 0.0f;
|
||||
if (sex == 0) {
|
||||
if (weight > (13.0f - (height * 0.5f)) * -1.0f) {
|
||||
float subsubcalc = ((height * 1.45f) + (height * 0.1158f) * height) - 120.0f;
|
||||
float subcalc = weight * 500.0f / subsubcalc;
|
||||
visceralFat = (subcalc - 6.0f) + (age * 0.07f);
|
||||
}
|
||||
else {
|
||||
float subcalc = 0.691f + (height * -0.0024f) + (height * -0.0024f);
|
||||
visceralFat = (((height * 0.027f) - (subcalc * weight)) * -1.0f) + (age * 0.07f) - age;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (height < weight * 1.6f) {
|
||||
float subcalc = ((height * 0.4f) - (height * (height * 0.0826f))) * -1.0f;
|
||||
visceralFat = ((weight * 305.0f) / (subcalc + 48.0f)) - 2.9f + (age * 0.15f);
|
||||
}
|
||||
else {
|
||||
float subcalc = 0.765f + height * -0.0015f;
|
||||
visceralFat = (((height * 0.143f) - (weight * subcalc)) * -1.0f) + (age * 0.15f) - 5.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return visceralFat;
|
||||
}
|
||||
|
||||
public float getBodyFat(float weight, float impedance) {
|
||||
float bodyFat = 0.0f;
|
||||
float lbmSub = 0.8f;
|
||||
|
||||
if (sex == 0 && age <= 49) {
|
||||
lbmSub = 9.25f;
|
||||
} else if (sex == 0) {
|
||||
lbmSub = 7.25f;
|
||||
}
|
||||
|
||||
float lbmCoeff = getLBMCoefficient(weight, impedance);
|
||||
float coeff = 1.0f;
|
||||
|
||||
if (sex == 1 && weight < 61.0f) {
|
||||
coeff = 0.98f;
|
||||
}
|
||||
else if (sex == 0 && weight > 60.0f) {
|
||||
coeff = 0.96f;
|
||||
|
||||
if (height > 160.0f) {
|
||||
coeff *= 1.03f;
|
||||
}
|
||||
} else if (sex == 0 && weight < 50.0f) {
|
||||
coeff = 1.02f;
|
||||
|
||||
if (height > 160.0f) {
|
||||
coeff *= 1.03f;
|
||||
}
|
||||
}
|
||||
|
||||
bodyFat = (1.0f - (((lbmCoeff - lbmSub) * coeff) / weight)) * 100.0f;
|
||||
|
||||
if (bodyFat > 63.0f) {
|
||||
bodyFat = 75.0f;
|
||||
}
|
||||
|
||||
return bodyFat;
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ package com.dzeio.openhealth.di
|
||||
|
||||
import android.content.Context
|
||||
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.water.WaterDao
|
||||
import com.dzeio.openhealth.data.weight.WeightDao
|
||||
@ -12,6 +14,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provide to the application the Database/Daos and external services
|
||||
*/
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DatabaseModule {
|
||||
@ -36,4 +41,15 @@ class DatabaseModule {
|
||||
fun provideStepsDao(appDatabase: AppDatabase): StepDao {
|
||||
return appDatabase.stepDao()
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideFoodDao(appDatabase: AppDatabase): FoodDao {
|
||||
return appDatabase.foodDao()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideOpenFoodFactService(): OpenFoodFactService {
|
||||
return OpenFoodFactService.getService()
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package com.dzeio.openhealth.di
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.dzeio.openhealth.utils.Bluetooth
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@ -11,6 +12,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provide to the application System elements
|
||||
*/
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class SystemModule {
|
||||
@ -26,4 +30,8 @@ class SystemModule {
|
||||
fun provideConfig(sharedPreferences: SharedPreferences): Configuration {
|
||||
return Configuration(sharedPreferences)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideBluetooth(@ApplicationContext context: Context): Bluetooth = Bluetooth(context)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package com.dzeio.openhealth.graphs
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.units.Units
|
||||
import com.dzeio.openhealth.utils.GraphUtils
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.mikephil.charting.components.LimitLine
|
||||
import com.github.mikephil.charting.components.YAxis
|
||||
import com.github.mikephil.charting.data.Entry
|
||||
import com.github.mikephil.charting.data.LineData
|
||||
import com.github.mikephil.charting.data.LineDataSet
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object WeightChart {
|
||||
fun setup(
|
||||
chart: LineChart,
|
||||
view: View,
|
||||
data: List<Weight>,
|
||||
modifier: Units.Mass,
|
||||
goal: Float?,
|
||||
limit: Boolean = true
|
||||
) {
|
||||
GraphUtils.lineChartSetup(
|
||||
chart,
|
||||
MaterialColors.getColor(
|
||||
view,
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
),
|
||||
MaterialColors.getColor(
|
||||
view,
|
||||
com.google.android.material.R.attr.colorOnBackground
|
||||
)
|
||||
)
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Axis Max/Min
|
||||
var axisMin = max(data.minOf { it.weight } - 10, 0f)
|
||||
var axisMax = data.maxOf { it.weight } + 10
|
||||
|
||||
if (goal != null) {
|
||||
axisMax = max(axisMax, goal)
|
||||
axisMin = min(axisMin, goal)
|
||||
}
|
||||
|
||||
// Average calculation
|
||||
val averageCalculation = min(30, max(3, data.size / 2))
|
||||
val isEven = averageCalculation % 2 == 1
|
||||
val midValue = averageCalculation / 2
|
||||
|
||||
val averageYs = data.mapIndexed { index, entry ->
|
||||
var minItem = index - midValue
|
||||
var maxItem = index + if (!isEven) midValue + 1 else midValue
|
||||
val lastEntry = data.size - 1
|
||||
if (minItem < 0) {
|
||||
maxItem += kotlin.math.abs(minItem)
|
||||
minItem = 0
|
||||
}
|
||||
if (maxItem >= lastEntry) {
|
||||
val diff = maxItem - lastEntry
|
||||
minItem = max(0, minItem - diff)
|
||||
maxItem -= diff
|
||||
}
|
||||
|
||||
var average = 0f
|
||||
for (i in minItem..maxItem) {
|
||||
average += data[i].weight
|
||||
}
|
||||
|
||||
return@mapIndexed Entry(
|
||||
entry.timestamp.toFloat(),
|
||||
(average / (maxItem - minItem + 1)) * modifier.modifier
|
||||
)
|
||||
}
|
||||
|
||||
val rawData = GraphUtils.lineDataSet(
|
||||
LineDataSet(
|
||||
data.mapIndexed { _, weight ->
|
||||
return@mapIndexed Entry(
|
||||
weight.timestamp.toFloat(),
|
||||
weight.weight * modifier.modifier
|
||||
)
|
||||
},
|
||||
"Weight"
|
||||
)
|
||||
).apply {
|
||||
axisDependency = YAxis.AxisDependency.RIGHT
|
||||
}
|
||||
|
||||
val averageData = GraphUtils.lineDataSet(LineDataSet(averageYs, "Average")).apply {
|
||||
axisDependency = YAxis.AxisDependency.RIGHT
|
||||
color = Color.GREEN
|
||||
}
|
||||
|
||||
val entries = ArrayList<Entry>()
|
||||
for (item in data) {
|
||||
entries.add(
|
||||
Entry(
|
||||
item.timestamp.toFloat(),
|
||||
item.weight * modifier.modifier
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
chart.apply {
|
||||
|
||||
this.data = LineData(rawData, averageData)
|
||||
|
||||
val twoWeeks = (data[data.size - 1].timestamp - data[0].timestamp) > 1290000000f
|
||||
|
||||
if (twoWeeks && limit) {
|
||||
// idk what I did but it works lol
|
||||
setVisibleXRange(
|
||||
0f, data[data.size - 1].timestamp / 1000f
|
||||
)
|
||||
|
||||
axisRight.axisMinimum = axisMin * modifier.modifier
|
||||
axisRight.axisMaximum = axisMax * modifier.modifier
|
||||
|
||||
// BIS... :(
|
||||
// Also it invalidate the view so I don't have to call invalidate
|
||||
moveViewToX(data[data.size - 1].timestamp - 1290000000f)
|
||||
}
|
||||
|
||||
if (goal != null) {
|
||||
val limit = LimitLine(goal * modifier.modifier)
|
||||
limit.lineColor = Color.RED
|
||||
val dash = 30f
|
||||
limit.enableDashedLine(dash, dash, 1f)
|
||||
limit.lineWidth = 1f
|
||||
limit.textColor = Color.BLACK
|
||||
|
||||
axisRight.removeAllLimitLines()
|
||||
axisRight.addLimitLine(limit)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,9 @@ package com.dzeio.openhealth.interfaces
|
||||
|
||||
import android.app.NotificationManager
|
||||
|
||||
/**
|
||||
* The different notification channels the applicaiton is using
|
||||
*/
|
||||
enum class NotificationChannels(
|
||||
val id: String,
|
||||
val channelName: String,
|
||||
|
@ -1,5 +1,8 @@
|
||||
package com.dzeio.openhealth.interfaces
|
||||
|
||||
/**
|
||||
* The different notifications the application can send to the user
|
||||
*/
|
||||
enum class NotificationIds {
|
||||
WaterIntake,
|
||||
|
||||
@ -7,4 +10,4 @@ enum class NotificationIds {
|
||||
* Open Health Main Service Notification ID
|
||||
*/
|
||||
Service
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import com.dzeio.openhealth.data.step.StepRepository_Factory
|
||||
import com.dzeio.openhealth.data.step.StepSource
|
||||
import com.dzeio.openhealth.interfaces.NotificationChannels
|
||||
import com.dzeio.openhealth.interfaces.NotificationIds
|
||||
import com.dzeio.openhealth.utils.polyfills.NotificationBehavior
|
||||
import com.dzeio.openhealth.utils.polyfills.stopForegroundPoly
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -29,12 +31,18 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
/**
|
||||
* The Service that allow the application to run in the background
|
||||
*/
|
||||
class OpenHealthService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "${Application.TAG}/Service"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the StepRepository without DI because it is unavailable here
|
||||
*/
|
||||
private val stepRepository: StepRepository
|
||||
get() = StepRepository_Factory.newInstance(
|
||||
AppDatabase.getInstance(applicationContext).stepDao()
|
||||
@ -64,42 +72,56 @@ class OpenHealthService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
scope.launch {
|
||||
// start the StepSource
|
||||
val source = StepSource(this@OpenHealthService)
|
||||
source.events.receiveAsFlow().collectLatest {
|
||||
Log.d(TAG, "Received value: $it")
|
||||
|
||||
// receive each updates as to the number of steps taken
|
||||
source.events.receiveAsFlow().collectLatest {
|
||||
// Log.d(TAG, "Received value: $it")
|
||||
|
||||
// handle case where no new steps were taken
|
||||
if (it <= 0f) {
|
||||
Log.d(TAG, "No new steps registered ($it)")
|
||||
return@collectLatest
|
||||
}
|
||||
Log.d(TAG, "New steps registered: $it")
|
||||
|
||||
// update internal variables to keep track of the number of steps taken
|
||||
// Log.d(TAG, "New steps registered: $it")
|
||||
stepsTaken += it.toUInt()
|
||||
stepsBuffer += it.toInt()
|
||||
|
||||
// show the notification
|
||||
showNotification()
|
||||
|
||||
// try to get the current number of steps for the hour from the DB
|
||||
val step = withTimeoutOrNull(1000) {
|
||||
return@withTimeoutOrNull stepRepository.currentStep().firstOrNull()
|
||||
}
|
||||
Log.d(TAG, "stepRepository: $step")
|
||||
|
||||
// Log.d(TAG, "stepRepository: $step")
|
||||
// if steps registered, add them and send them back
|
||||
if (step != null) {
|
||||
step.value += stepsBuffer
|
||||
stepRepository.updateStep(step)
|
||||
// create a new steps object and send it
|
||||
} else {
|
||||
stepRepository.addStep(Step(value = stepsBuffer))
|
||||
}
|
||||
|
||||
// reset the internal buffer
|
||||
stepsBuffer = 0
|
||||
Log.d(TAG, "Added step!")
|
||||
// Log.d(TAG, "Added step!")
|
||||
}
|
||||
}
|
||||
|
||||
// Display a notification about us starting. We put an icon in the status bar.
|
||||
|
||||
startForeground(NotificationIds.Service.ordinal, showNotification())
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopForeground(true)
|
||||
stopForegroundPoly(NotificationBehavior.REMOVE)
|
||||
|
||||
// Tell the user we stopped.
|
||||
Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show()
|
||||
|
@ -3,13 +3,11 @@ package com.dzeio.openhealth.ui
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.BuildConfig
|
||||
import com.dzeio.openhealth.core.BaseActivity
|
||||
import com.dzeio.openhealth.databinding.ActivityErrorBinding
|
||||
import kotlin.system.exitProcess
|
||||
@ -28,20 +26,8 @@ class ErrorActivity : BaseActivity<ActivityErrorBinding>() {
|
||||
|
||||
val data = intent.getStringExtra("error")
|
||||
|
||||
// Get Application datas
|
||||
val deviceToReport = if (Build.DEVICE.contains(Build.MANUFACTURER)) Build.DEVICE else "${Build.MANUFACTURER} ${Build.DEVICE}"
|
||||
|
||||
val reportText = """
|
||||
Crash Report (Thread: ${intent?.getLongExtra("threadId", -1) ?: "unknown"})
|
||||
${BuildConfig.APPLICATION_ID} v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})
|
||||
on $deviceToReport (${Build.MODEL}) running Android ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT})
|
||||
|
||||
backtrace:
|
||||
|
||||
""".trimIndent() + data
|
||||
|
||||
// put it in the textView
|
||||
binding.errorText.text = reportText
|
||||
binding.errorText.text = data
|
||||
|
||||
// Handle the Quit button
|
||||
binding.errorQuit.setOnClickListener {
|
||||
@ -51,14 +37,15 @@ class ErrorActivity : BaseActivity<ActivityErrorBinding>() {
|
||||
|
||||
// Handle the Email Button
|
||||
binding.errorSubmitEmail.setOnClickListener {
|
||||
|
||||
// Create Intent
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.data = Uri.parse("mailto:")
|
||||
intent.type = "text/plain"
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(
|
||||
Uri.parse("mailto:"),
|
||||
"text/plain"
|
||||
)
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("report.openhealth@dzeio.com"))
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "Error report for application crash")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, "Send Report Email\n$reportText")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, "Send Report Email\n$data")
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, "Send Report Email..."))
|
||||
@ -69,14 +56,17 @@ class ErrorActivity : BaseActivity<ActivityErrorBinding>() {
|
||||
|
||||
// Handle the GitHub Button
|
||||
binding.errorSubmitGithub.setOnClickListener {
|
||||
|
||||
// Build URL
|
||||
val url = "https://github.com/dzeiocom/OpenHealth/issues/new?title=Application Error&body=$reportText"
|
||||
val url =
|
||||
"https://github.com/dzeiocom/OpenHealth/issues/new?" +
|
||||
"title=Application Error&" +
|
||||
"body=${data?.replace("\n", "\\n")}"
|
||||
|
||||
try {
|
||||
startActivity(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setData(Uri.parse(url))
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(this, "No Web Browser found :(", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package com.dzeio.openhealth.ui
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@ -11,6 +10,7 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.updatePadding
|
||||
@ -21,7 +21,6 @@ import androidx.navigation.ui.NavigationUI
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseActivity
|
||||
import com.dzeio.openhealth.databinding.ActivityMainBinding
|
||||
@ -35,7 +34,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "${Application.TAG}/MainActivity"
|
||||
val TAG: String = this::class.java.simpleName
|
||||
}
|
||||
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
@ -48,7 +47,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(R.style.Theme_OpenHealth_NoActionBar)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
}
|
||||
|
||||
override fun onCreated(savedInstanceState: Bundle?) {
|
||||
@ -58,7 +56,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
// Comportement chelou API 28-
|
||||
// Comportement normal 31+
|
||||
|
||||
// do not do the cool status/navigation bars for API 29 & 30
|
||||
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.R && Build.VERSION.SDK_INT != Build.VERSION_CODES.Q) {
|
||||
// allow to put the content behind the status bar & Navigation bar (one of them at least lul)
|
||||
@ -66,25 +63,23 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
|
||||
// Make the color of the navigation bar semi-transparent
|
||||
// window.navigationBarColor = Color.TRANSPARENT
|
||||
// Make the color of the status bar transparent
|
||||
// window.statusBarColor = Color.TRANSPARENT
|
||||
// Apply the previous changes
|
||||
// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
|
||||
|
||||
// Update toolbar height with the statusbar size included
|
||||
// ALSO: make both the status/navigation bars transparent (WHYYYYYYY)
|
||||
val toolbarHeight = binding.toolbar.layoutParams.height
|
||||
window.decorView.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val statusBarSize = insets.systemWindowInsetTop
|
||||
// Use getInsets(int) with WindowInsets.Type.systemBars() instead.
|
||||
val statusBarSize = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
insets.getInsets(WindowInsets.Type.systemBars()).top
|
||||
} else {
|
||||
insets.systemWindowInsetTop
|
||||
}
|
||||
// Add padding to the toolbar (YaY I know how something works)
|
||||
binding.toolbar.updatePadding(top = statusBarSize)
|
||||
binding.toolbar.layoutParams.height = toolbarHeight + statusBarSize
|
||||
return@setOnApplyWindowInsetsListener insets
|
||||
}
|
||||
|
||||
// normally makes sure icons are at the correct color but idk if it works
|
||||
// normally makes sure icons are at the correct color
|
||||
when (this.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> {
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
@ -121,25 +116,17 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
binding.bottomNav.setupWithNavController(navController)
|
||||
|
||||
// registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
//
|
||||
// }
|
||||
// .launch(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
|
||||
createNotificationChannel()
|
||||
|
||||
// Services
|
||||
WaterReminderWorker.setup(this)
|
||||
// StepCountService.setup(this)
|
||||
|
||||
ServiceUtils.startService(this, OpenHealthService::class.java)
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.main, menu)
|
||||
return true
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean =
|
||||
@ -149,22 +136,13 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
NavigationUI.onNavDestinationSelected(item, navController) ||
|
||||
super.onOptionsItemSelected(item)
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
Log.d("MainActivity", "onActivityResult $requestCode $resultCode")
|
||||
for (fragment in supportFragmentManager.primaryNavigationFragment!!.childFragmentManager.fragments) {
|
||||
fragment.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
val notificationManager: NotificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
for (channel in NotificationChannels.values()) {
|
||||
Log.d("MainActivity", channel.channelName)
|
||||
Log.d(TAG, channel.channelName)
|
||||
try {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
@ -174,7 +152,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error Creating Notification Channel", e)
|
||||
Log.e(TAG, "Error Creating Notification Channel", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ import com.dzeio.openhealth.core.BaseStaticFragment
|
||||
import com.dzeio.openhealth.databinding.FragmentAboutBinding
|
||||
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
|
||||
|
||||
/**
|
||||
* Fragment for the About page
|
||||
*/
|
||||
class AboutFragment : BaseStaticFragment<FragmentAboutBinding>() {
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentAboutBinding
|
||||
get() = FragmentAboutBinding::inflate
|
||||
@ -23,22 +26,29 @@ class AboutFragment : BaseStaticFragment<FragmentAboutBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// set the version number
|
||||
binding.version.text =
|
||||
resources.getString(R.string.version_number, BuildConfig.VERSION_NAME)
|
||||
|
||||
// handle contact US button
|
||||
binding.contactUs.setOnClickListener {
|
||||
openLink("mailto:context.openhealth@dze.io")
|
||||
openLink("mailto:contact.openhealth@dze.io")
|
||||
}
|
||||
|
||||
// handle Github button
|
||||
binding.github.setOnClickListener {
|
||||
openLink("https://github.com/dzeiocom/OpenHealth")
|
||||
}
|
||||
|
||||
// send the user to the Google OSS licenses page when clicked
|
||||
binding.licenses.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* simple function that try to open a link.
|
||||
*/
|
||||
private fun openLink(url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
|
@ -10,6 +10,9 @@ import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.databinding.FragmentActivityBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* Fragment for the Activity page
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ActivityFragment :
|
||||
BaseFragment<ActivityViewModel, FragmentActivityBinding>(ActivityViewModel::class.java) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.dzeio.openhealth.ui.browse
|
||||
|
||||
import android.Manifest
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@ -10,7 +9,6 @@ import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.databinding.FragmentBrowseBinding
|
||||
@ -25,10 +23,6 @@ class BrowseFragment :
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentBrowseBinding
|
||||
get() = FragmentBrowseBinding::inflate
|
||||
|
||||
private val settings: SharedPreferences by lazy {
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
}
|
||||
|
||||
private lateinit var button: MaterialCardView
|
||||
private val activityResult = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
@ -45,21 +39,32 @@ class BrowseFragment :
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// handle clicking on the weight card
|
||||
binding.weight.setOnClickListener {
|
||||
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavListWeight())
|
||||
}
|
||||
|
||||
// handle clicking on the water intake card
|
||||
binding.waterIntake.setOnClickListener {
|
||||
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome())
|
||||
}
|
||||
|
||||
// handle clicking on the food calories card
|
||||
binding.foodCalories.setOnClickListener {
|
||||
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToFoodHomeFragment())
|
||||
}
|
||||
|
||||
// handle clicking on the steps card
|
||||
binding.steps.setOnClickListener {
|
||||
// since Android Q We need additionnal permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// check for activity permission
|
||||
val activityPermission = PermissionsManager.hasPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACTIVITY_RECOGNITION
|
||||
)
|
||||
|
||||
// check for notification permission
|
||||
val notificationPermission =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionsManager.hasPermission(
|
||||
requireContext(),
|
||||
@ -68,38 +73,56 @@ class BrowseFragment :
|
||||
|
||||
val permissionsToAsk = arrayListOf<String>()
|
||||
|
||||
// add missing permission to list
|
||||
if (!activityPermission) {
|
||||
permissionsToAsk.add(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
}
|
||||
|
||||
// add missing permission to list only if necessary
|
||||
if (!notificationPermission && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionsToAsk.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
// ask for permissions
|
||||
if (permissionsToAsk.isNotEmpty()) {
|
||||
button = binding.steps
|
||||
activityResult.launch(permissionsToAsk.toTypedArray())
|
||||
return@setOnClickListener
|
||||
}
|
||||
}
|
||||
|
||||
// navigate user to the Steps home fragment
|
||||
findNavController().navigate(
|
||||
BrowseFragmentDirections.actionNavBrowseToStepsHomeFragment()
|
||||
)
|
||||
}
|
||||
|
||||
// display the number of steps the user made today
|
||||
viewModel.steps.observe(viewLifecycleOwner) {
|
||||
binding.stepsText.setText("$it of xxx steps")
|
||||
updateStepsText(it, viewModel.stepsGoal.value)
|
||||
}
|
||||
|
||||
// display the number of steps the user should do today
|
||||
viewModel.stepsGoal.observe(viewLifecycleOwner) {
|
||||
updateStepsText(viewModel.steps.value, it)
|
||||
}
|
||||
|
||||
// display the current user's weight
|
||||
viewModel.weight.observe(viewLifecycleOwner) {
|
||||
binding.weightText.setText(
|
||||
String.format(
|
||||
resources.getString(R.string.weight_current),
|
||||
it,
|
||||
resources.getString(R.string.unit_mass_kilogram_unit)
|
||||
String.format(resources.getString(R.string.unit_mass_kilogram_unit), it)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStepsText(numberOfSteps: Int?, goal: Int?) {
|
||||
var text = "${numberOfSteps ?: 0} steps"
|
||||
if (goal != null) {
|
||||
text = "${numberOfSteps ?: 0} of $goal steps"
|
||||
}
|
||||
binding.stepsText.setText(text)
|
||||
}
|
||||
}
|
||||
|
@ -3,23 +3,28 @@ package com.dzeio.openhealth.ui.browse
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dzeio.openhealth.Settings
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.step.StepRepository
|
||||
import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BrowseViewModel @Inject internal constructor(
|
||||
stepRepository: StepRepository,
|
||||
weightRepository: WeightRepository
|
||||
weightRepository: WeightRepository,
|
||||
config: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _steps = MutableLiveData(0)
|
||||
val steps: LiveData<Int> = _steps
|
||||
|
||||
val stepsGoal = config.getInt(Settings.STEPS_GOAL).toLiveData()
|
||||
|
||||
private val _weight = MutableLiveData(0f)
|
||||
val weight: LiveData<Float> = _weight
|
||||
|
||||
|
@ -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()
|
||||
|
||||
}
|
94
app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialog.kt
Normal file
94
app/src/main/java/com/dzeio/openhealth/ui/food/FoodDialog.kt
Normal file
@ -0,0 +1,94 @@
|
||||
package com.dzeio.openhealth.ui.food
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseDialog
|
||||
import com.dzeio.openhealth.databinding.DialogFoodProductBinding
|
||||
import com.dzeio.openhealth.utils.NetworkUtils
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* Dialog that display to the user a spcific product and it's consumption
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class FoodDialog :
|
||||
BaseDialog<FoodDialogViewModel, DialogFoodProductBinding>(FoodDialogViewModel::class.java) {
|
||||
|
||||
private val args: FoodDialogArgs by navArgs()
|
||||
|
||||
private var quantity: Float? = null
|
||||
|
||||
override val bindingInflater: (LayoutInflater) -> DialogFoodProductBinding =
|
||||
DialogFoodProductBinding::inflate
|
||||
|
||||
override fun onBuilderInit(builder: MaterialAlertDialogBuilder) {
|
||||
super.onBuilderInit(builder)
|
||||
|
||||
builder.apply {
|
||||
setTitle("Product")
|
||||
setIcon(R.drawable.ic_outline_fastfood_24)
|
||||
setPositiveButton(R.string.validate) { dialog, _ ->
|
||||
if (quantity != null) {
|
||||
viewModel.saveNewQuantity(quantity!!)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
if (args.deleteOnCancel) {
|
||||
viewModel.delete()
|
||||
}
|
||||
dialog.cancel()
|
||||
}
|
||||
setNeutralButton(R.string.delete) { _, _ ->
|
||||
viewModel.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreated() {
|
||||
super.onCreated()
|
||||
Log.d("FoodDialog", args.id.toString())
|
||||
viewModel.init(args.id)
|
||||
|
||||
viewModel.items.observe(this) {
|
||||
Log.d("FoodDialog", it.toString())
|
||||
updateGraphs(null)
|
||||
|
||||
binding.serving.text = "Serving: ${it.serving}"
|
||||
if (it.image != null) {
|
||||
NetworkUtils.getImageInBackground(
|
||||
binding.image,
|
||||
it.image!!
|
||||
)
|
||||
}
|
||||
binding.quantity.setText(it.quantity.toString())
|
||||
}
|
||||
|
||||
binding.quantity.addTextChangedListener {
|
||||
updateGraphs(binding.quantity.text.toString().toFloatOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGraphs(newQuantity: Float?) {
|
||||
quantity = newQuantity
|
||||
viewModel.items.value?.let {
|
||||
val transformer = newQuantity ?: it.quantity
|
||||
val energy = it.energy / 100 * transformer
|
||||
binding.energyTxt.text = "${energy.toInt()} / 2594kcal"
|
||||
binding.energyBar.progress = (100 * energy / 2594).toInt()
|
||||
val proteins = it.proteins / 100 * transformer
|
||||
binding.proteinsTxt.text = "${proteins.toInt()} / 130g"
|
||||
binding.proteinsBar.progress = (100 * proteins / 130).toInt()
|
||||
val carbohydrates = it.carbohydrates / 100 * transformer
|
||||
binding.carbsTxt.text = "${carbohydrates.toInt()} / 324g"
|
||||
binding.carbsBar.progress = (100 * carbohydrates / 324).toInt()
|
||||
val fat = it.fat / 100 * transformer
|
||||
binding.fatTxt.text = "${fat.toInt()} / 87g"
|
||||
binding.fatBar.progress = (100 * fat / 87).toInt()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.dzeio.openhealth.ui.food
|
||||
|
||||
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 javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class FoodDialogViewModel @Inject internal constructor(
|
||||
private val foodRepository: FoodRepository
|
||||
) : BaseViewModel() {
|
||||
val items: MutableLiveData<Food> = MutableLiveData()
|
||||
|
||||
fun init(productId: Long) {
|
||||
viewModelScope.launch {
|
||||
val res = foodRepository.getById(productId)
|
||||
val food = res.first()
|
||||
if (food != null) {
|
||||
items.postValue(food!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
viewModelScope.launch {
|
||||
val item = items.value
|
||||
if (item != null) {
|
||||
foodRepository.delete(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNewQuantity(quantity: Float) {
|
||||
val it = items.value
|
||||
if (it == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val transformer = quantity / it.quantity
|
||||
it.energy = it.energy * transformer
|
||||
it.proteins = it.proteins * transformer
|
||||
it.carbohydrates = it.carbohydrates * transformer
|
||||
it.fat = it.fat * transformer
|
||||
it.quantity = quantity
|
||||
|
||||
viewModelScope.launch {
|
||||
foodRepository.update(it)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package com.dzeio.openhealth.ui.food
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.R
|
||||
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
|
||||
import java.util.Calendar
|
||||
|
||||
@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)
|
||||
|
||||
// FIXME: deprecated
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
viewModel.init()
|
||||
|
||||
val recycler = binding.list
|
||||
|
||||
val manager = LinearLayoutManager(requireContext())
|
||||
recycler.layoutManager = manager
|
||||
|
||||
val adapter = FoodAdapter()
|
||||
adapter.onItemClick = {
|
||||
findNavController().navigate(
|
||||
FoodHomeFragmentDirections.actionFoodHomeFragmentToNavDialogFoodProduct(
|
||||
it.id,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
recycler.adapter = adapter
|
||||
|
||||
viewModel.items.observe(viewLifecycleOwner) {
|
||||
adapter.set(it)
|
||||
|
||||
var energy = 0f
|
||||
var proteins = 0f
|
||||
var carbohydrates = 0f
|
||||
var fat = 0f
|
||||
for (food in it) {
|
||||
energy += food.energy / 100 * food.quantity
|
||||
proteins += food.proteins / 100 * food.quantity
|
||||
carbohydrates += food.carbohydrates / 100 * food.quantity
|
||||
fat += food.fat / 100 * food.quantity
|
||||
}
|
||||
binding.energyTxt.text = "${energy.toInt()} / 2594kcal"
|
||||
binding.energyBar.progress = (100 * energy / 2594).toInt()
|
||||
binding.proteinsTxt.text = "${proteins.toInt()} / 130g"
|
||||
binding.proteinsBar.progress = (100 * proteins / 130).toInt()
|
||||
binding.carbsTxt.text = "${carbohydrates.toInt()} / 324g"
|
||||
binding.carbsBar.progress = (100 * carbohydrates / 324).toInt()
|
||||
binding.fatTxt.text = "${fat.toInt()} / 87g"
|
||||
binding.fatBar.progress = (100 * fat / 87).toInt()
|
||||
}
|
||||
|
||||
binding.next.setOnClickListener {
|
||||
viewModel.next()
|
||||
}
|
||||
|
||||
binding.previous.setOnClickListener {
|
||||
viewModel.previous()
|
||||
}
|
||||
|
||||
viewModel.date.observe(viewLifecycleOwner) {
|
||||
val date = Calendar.getInstance()
|
||||
date.timeInMillis = it
|
||||
binding.date.text = "${date.get(Calendar.YEAR)}-${date.get(Calendar.MONTH) + 1}-${date.get(
|
||||
Calendar.DAY_OF_MONTH
|
||||
)}"
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
menu.findItem(R.id.action_add).isVisible = true
|
||||
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_add -> {
|
||||
findNavController().navigate(
|
||||
FoodHomeFragmentDirections.actionFoodHomeFragmentToNavDialogFoodSearch()
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
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 java.util.Calendar
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class FoodHomeViewModel @Inject internal constructor(
|
||||
private val foodRepository: FoodRepository
|
||||
) : BaseViewModel() {
|
||||
val items: MutableLiveData<List<Food>> = MutableLiveData()
|
||||
private val list: MutableLiveData<List<Food>> = MutableLiveData(arrayListOf())
|
||||
val date: MutableLiveData<Long> = MutableLiveData(Calendar.getInstance().timeInMillis)
|
||||
|
||||
fun init() {
|
||||
val now = Calendar.getInstance()
|
||||
now.set(Calendar.HOUR, 0)
|
||||
now.set(Calendar.MINUTE, 0)
|
||||
now.set(Calendar.SECOND, 0)
|
||||
date.postValue(now.timeInMillis)
|
||||
viewModelScope.launch {
|
||||
foodRepository.getAll().collectLatest {
|
||||
list.postValue(it)
|
||||
updateList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun next() {
|
||||
val now = Calendar.getInstance()
|
||||
now.timeInMillis = date.value!!
|
||||
now.add(Calendar.DAY_OF_YEAR, 1)
|
||||
|
||||
date.value = now.timeInMillis
|
||||
|
||||
updateList()
|
||||
}
|
||||
|
||||
fun previous() {
|
||||
val now = Calendar.getInstance()
|
||||
now.timeInMillis = date.value!!
|
||||
now.add(Calendar.DAY_OF_YEAR, -1)
|
||||
|
||||
date.value = now.timeInMillis
|
||||
|
||||
updateList()
|
||||
}
|
||||
|
||||
private fun updateList(foods: List<Food>? = null) {
|
||||
val day = Calendar.getInstance()
|
||||
day.timeInMillis = date.value!!
|
||||
val todayInMillis = day.timeInMillis
|
||||
day.add(Calendar.DAY_OF_YEAR, 1)
|
||||
val tomorrow = day.timeInMillis
|
||||
|
||||
items.postValue(
|
||||
(foods ?: list.value!!).filter { food ->
|
||||
food.timestamp in todayInMillis until tomorrow
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package com.dzeio.openhealth.ui.food
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.adapters.FoodAdapter
|
||||
import com.dzeio.openhealth.core.BaseDialog
|
||||
import com.dzeio.openhealth.databinding.DialogFoodSearchProductBinding
|
||||
import com.dzeio.openhealth.utils.NetworkResult
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SearchFoodDialog :
|
||||
BaseDialog<SearchFoodDialogViewModel, DialogFoodSearchProductBinding>(
|
||||
SearchFoodDialogViewModel::class.java
|
||||
) {
|
||||
|
||||
override val bindingInflater: (LayoutInflater) -> DialogFoodSearchProductBinding =
|
||||
DialogFoodSearchProductBinding::inflate
|
||||
|
||||
override fun onBuilderInit(builder: MaterialAlertDialogBuilder) {
|
||||
super.onBuilderInit(builder)
|
||||
|
||||
builder.apply {
|
||||
setTitle("Add Product")
|
||||
setIcon(R.drawable.ic_outline_fastfood_24)
|
||||
setNegativeButton(R.string.close) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
setNeutralButton("Search", null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogInit(dialog: AlertDialog) {
|
||||
super.onDialogInit(dialog)
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val btn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
||||
btn.setOnClickListener {
|
||||
viewModel.search(binding.input.text.toString())
|
||||
binding.loading.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreated() {
|
||||
super.onCreated()
|
||||
|
||||
val recycler = binding.list
|
||||
|
||||
val manager = LinearLayoutManager(requireContext())
|
||||
recycler.layoutManager = manager
|
||||
|
||||
val adapter = FoodAdapter()
|
||||
adapter.onItemClick = {
|
||||
lifecycleScope.launch {
|
||||
val id = viewModel.addProduct(it)
|
||||
findNavController().navigate(
|
||||
SearchFoodDialogDirections.actionNavDialogFoodSearchToNavDialogFoodProduct(
|
||||
id,
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
recycler.adapter = adapter
|
||||
|
||||
viewModel.items.observe(this) {
|
||||
adapter.set(it.data ?: arrayListOf())
|
||||
if (it.status == NetworkResult.NetworkStatus.FINISHED) {
|
||||
binding.loading.visibility = View.GONE
|
||||
} else if (it.status == NetworkResult.NetworkStatus.ERRORED) {
|
||||
binding.errorText.visibility = View.VISIBLE
|
||||
binding.loading.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.dzeio.openhealth.ui.food
|
||||
|
||||
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 com.dzeio.openhealth.utils.NetworkResult
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class SearchFoodDialogViewModel @Inject internal constructor(
|
||||
private val foodRepository: FoodRepository
|
||||
) : BaseViewModel() {
|
||||
val items: MutableLiveData<NetworkResult<List<Food>>> = MutableLiveData()
|
||||
|
||||
fun search(text: String) {
|
||||
viewModelScope.launch {
|
||||
foodRepository.searchFood(text).collectLatest {
|
||||
items.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addProduct(product: Food): Long {
|
||||
if (product.id > 0) {
|
||||
product.id = 0
|
||||
}
|
||||
return foodRepository.add(product)
|
||||
}
|
||||
}
|
@ -4,20 +4,25 @@ import android.animation.ValueAnimator
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.dzeio.charts.Entry
|
||||
import com.dzeio.charts.axis.Line
|
||||
import com.dzeio.charts.series.LineSerie
|
||||
import com.dzeio.openhealth.BuildConfig
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.databinding.FragmentHomeBinding
|
||||
import com.dzeio.openhealth.graphs.WeightChart
|
||||
import com.dzeio.openhealth.ui.weight.WeightDialog
|
||||
import com.dzeio.openhealth.units.Units
|
||||
import com.dzeio.openhealth.utils.ChartUtils
|
||||
import com.dzeio.openhealth.utils.DrawUtils
|
||||
import com.dzeio.openhealth.utils.GraphUtils
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.max
|
||||
@ -44,6 +49,9 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Water Intake
|
||||
*/
|
||||
binding.fragmentHomeWaterAdd.setOnClickListener {
|
||||
val water = viewModel.water.value
|
||||
if (water == null || !water.isToday()) {
|
||||
@ -76,26 +84,34 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
}
|
||||
}
|
||||
|
||||
// handle button to go to weight home
|
||||
binding.listWeight.setOnClickListener {
|
||||
findNavController().navigate(HomeFragmentDirections.actionNavHomeToNavListWeight())
|
||||
}
|
||||
|
||||
// handle button to go to water intake home
|
||||
binding.gotoWaterHome.setOnClickListener {
|
||||
findNavController().navigate(HomeFragmentDirections.actionNavHomeToNavWaterHome())
|
||||
}
|
||||
|
||||
GraphUtils.lineChartSetup(
|
||||
binding.weightGraph,
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
),
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnBackground
|
||||
)
|
||||
)
|
||||
binding.weightGraph.apply {
|
||||
val serie = LineSerie(this)
|
||||
ChartUtils.materielTheme(this, requireView())
|
||||
series = arrayListOf(serie)
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
binding.gotoTests.apply {
|
||||
visibility = View.VISIBLE
|
||||
setOnClickListener {
|
||||
findNavController().navigate(
|
||||
HomeFragmentDirections.actionNavHomeToTestsFragment()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the water intake Graph when the water intake changes
|
||||
viewModel.water.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
updateWater(it.value)
|
||||
@ -104,63 +120,101 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
}
|
||||
}
|
||||
|
||||
// Update the steps Graph when the steps count changes
|
||||
viewModel.steps.observe(viewLifecycleOwner) {
|
||||
binding.stepsCurrent.text = it.toString()
|
||||
}
|
||||
|
||||
// Update the steps Graph when the goal changes
|
||||
viewModel.stepsGoal.observe(viewLifecycleOwner) {
|
||||
if (it == null) {
|
||||
binding.stepsTotal.text = ""
|
||||
return@observe
|
||||
}
|
||||
binding.stepsTotal.text = it.toString()
|
||||
}
|
||||
|
||||
// update the graph when the weight changes
|
||||
viewModel.weights.observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
updateGraph(it)
|
||||
}
|
||||
}
|
||||
|
||||
// update the graph when the goal weight change
|
||||
viewModel.goalWeight.observe(viewLifecycleOwner) {
|
||||
if (viewModel.weights.value != null) updateGraph(viewModel.weights.value!!)
|
||||
}
|
||||
|
||||
// update the graph when the weight unit change
|
||||
viewModel.massUnit.observe(viewLifecycleOwner) {
|
||||
if (viewModel.weights.value != null) updateGraph(viewModel.weights.value!!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that update the graph for the weight
|
||||
*/
|
||||
private fun updateGraph(list: List<Weight>) {
|
||||
WeightChart.setup(
|
||||
binding.weightGraph,
|
||||
requireView(),
|
||||
list,
|
||||
viewModel.massUnit.value!!,
|
||||
viewModel.goalWeight.value
|
||||
)
|
||||
val chart = binding.weightGraph
|
||||
val serie = chart.series[0] as LineSerie
|
||||
|
||||
// legend.apply {
|
||||
// isEnabled = true
|
||||
// form = Legend.LegendForm.LINE
|
||||
//
|
||||
// if (goal != null) {
|
||||
// val legendEntry = LegendEntry().apply {
|
||||
// label = "Weight Goal"
|
||||
// formColor = Color.RED
|
||||
// }
|
||||
// setCustom(arrayOf(legendEntry))
|
||||
// }
|
||||
// }
|
||||
val entries: ArrayList<Entry> = arrayListOf()
|
||||
|
||||
list.forEach {
|
||||
entries.add(
|
||||
Entry(
|
||||
it.timestamp.toDouble(),
|
||||
it.weight
|
||||
)
|
||||
)
|
||||
}
|
||||
serie.entries = entries
|
||||
|
||||
if (viewModel.goalWeight.value != null) {
|
||||
chart.yAxis.addLine(
|
||||
viewModel.goalWeight.value!!,
|
||||
Line(true, Paint(chart.yAxis.linePaint).apply { strokeWidth = 4f })
|
||||
)
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
chart.xAxis.x = 0.0
|
||||
} else {
|
||||
chart.xAxis.x = list[0].timestamp.toDouble()
|
||||
}
|
||||
|
||||
chart.refresh()
|
||||
}
|
||||
|
||||
/**
|
||||
* the waterintake old value to keep for value update
|
||||
*/
|
||||
private var oldValue = 0f
|
||||
|
||||
/**
|
||||
* function that update the water count in the home page
|
||||
*/
|
||||
private fun updateWater(newValue: Int) {
|
||||
|
||||
// get the current Unit
|
||||
val waterUnit =
|
||||
Units.Volume.find(settings.getString("water_unit", "milliliter") ?: "Milliliter")
|
||||
|
||||
// Update the count
|
||||
binding.fragmentHomeWaterCurrent.text =
|
||||
String.format(
|
||||
resources.getString(waterUnit.unit),
|
||||
(newValue * waterUnit.modifier).toInt()
|
||||
)
|
||||
|
||||
// TODO: move it elsewhere
|
||||
binding.fragmentHomeWaterTotal.text =
|
||||
String.format(
|
||||
resources.getString(waterUnit.unit),
|
||||
viewModel.dailyWaterIntake
|
||||
)
|
||||
|
||||
// get the with/height of the ImageView
|
||||
var width = 1500
|
||||
var height = 750
|
||||
|
||||
@ -169,55 +223,41 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
height = binding.background.height
|
||||
}
|
||||
|
||||
val graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
|
||||
val canvas = Canvas(graph)
|
||||
val rect = RectF(
|
||||
10f,
|
||||
15f,
|
||||
90f,
|
||||
85f
|
||||
)
|
||||
|
||||
// DrawUtils.drawRect(
|
||||
// canvas,
|
||||
// RectF(
|
||||
// 0f,
|
||||
// 0f,
|
||||
// 100f,
|
||||
// 100f
|
||||
// ),
|
||||
// MaterialColors.getColor(
|
||||
// requireView(),
|
||||
// com.google.android.material.R.attr.colorOnPrimary
|
||||
// ),
|
||||
// 3f
|
||||
// )
|
||||
|
||||
DrawUtils.drawArc(
|
||||
canvas,
|
||||
100f,
|
||||
rect,
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimary
|
||||
),
|
||||
3f
|
||||
)
|
||||
|
||||
// Prepare the update animation
|
||||
val animator = ValueAnimator.ofInt(
|
||||
min(this.oldValue, viewModel.dailyWaterIntake.toFloat()).toInt(),
|
||||
min(newValue, viewModel.dailyWaterIntake)
|
||||
this.oldValue.toInt(),
|
||||
newValue
|
||||
)
|
||||
animator.duration = 300 // ms
|
||||
animator.addUpdateListener {
|
||||
this.oldValue = (it.animatedValue as Int).toFloat()
|
||||
val value = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake
|
||||
|
||||
this.oldValue = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat()
|
||||
// Log.d("Test2", "${this.oldValue}")
|
||||
val graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(graph)
|
||||
val rect = RectF(
|
||||
10f,
|
||||
15f,
|
||||
90f,
|
||||
85f
|
||||
)
|
||||
|
||||
// background Arc
|
||||
DrawUtils.drawArc(
|
||||
canvas,
|
||||
max(this.oldValue, 1f),
|
||||
100f,
|
||||
rect,
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimary
|
||||
),
|
||||
3f
|
||||
)
|
||||
|
||||
// Draw the big Arc
|
||||
DrawUtils.drawArc(
|
||||
canvas,
|
||||
min(max(value, 0.01f), 100f),
|
||||
rect,
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
@ -225,9 +265,15 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
),
|
||||
6f
|
||||
)
|
||||
|
||||
// save the canvas
|
||||
canvas.save()
|
||||
|
||||
// send it
|
||||
binding.background.setImageBitmap(graph)
|
||||
}
|
||||
|
||||
// start the animation
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
@ -1,58 +1,94 @@
|
||||
package com.dzeio.openhealth.ui.home
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dzeio.openhealth.Settings
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.step.StepRepository
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
import com.dzeio.openhealth.data.water.WaterRepository
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
import com.dzeio.openhealth.units.UnitFactory
|
||||
import com.dzeio.openhealth.units.Units
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject internal constructor(
|
||||
private val weightRepository: WeightRepository,
|
||||
private val waterRepository: WaterRepository,
|
||||
settings: SharedPreferences,
|
||||
stepRepository: StepRepository,
|
||||
config: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _steps = MutableLiveData(0)
|
||||
|
||||
/**
|
||||
* Steps taken today by the user
|
||||
*/
|
||||
val steps: LiveData<Int> = _steps
|
||||
|
||||
/**
|
||||
* Number of steps the use should do today
|
||||
*/
|
||||
val stepsGoal: LiveData<Int?> = config.getInt(Settings.STEPS_GOAL).toLiveData()
|
||||
|
||||
private val _water = MutableLiveData<Water?>(null)
|
||||
|
||||
/**
|
||||
* Quantity of water the user drank today
|
||||
*/
|
||||
val water: LiveData<Water?> = _water
|
||||
|
||||
private val _weights = MutableLiveData<List<Weight>?>(null)
|
||||
|
||||
/**
|
||||
* The list of weight of the user
|
||||
*/
|
||||
val weights: LiveData<List<Weight>?> = _weights
|
||||
|
||||
/**
|
||||
* The size of a cup for the quick water intake add
|
||||
*/
|
||||
var waterCupSize = config.getInt("water_cup_size").toLiveData()
|
||||
|
||||
var waterUnit =
|
||||
UnitFactory.volume(settings.getString("water_unit", "milliliter") ?: "Milliliter")
|
||||
/**
|
||||
* The unit used to display the water intake of the user
|
||||
*/
|
||||
var waterUnit = Units.Volume.find(config.getString("water_unit").value ?: "ml")
|
||||
|
||||
private val _massUnit = MutableLiveData(Units.Mass.KILOGRAM)
|
||||
|
||||
/**
|
||||
* The Mass unit used by the user
|
||||
*/
|
||||
val massUnit: LiveData<Units.Mass> = _massUnit
|
||||
|
||||
/**
|
||||
* the User weight goal
|
||||
*/
|
||||
val goalWeight = config.getFloat(Settings.WEIGHT_GOAL).toLiveData()
|
||||
|
||||
val dailyWaterIntake: Int =
|
||||
((settings.getString("water_intake", "1200")?.toFloatOrNull() ?: 1200f) * waterUnit.modifier)
|
||||
.toInt()
|
||||
val dailyWaterIntake: Float = (config.getFloat("water_intake").value ?: 1200f) * waterUnit.modifier
|
||||
|
||||
init {
|
||||
// Fetch today's water intake
|
||||
viewModelScope.launch {
|
||||
waterRepository.todayWater().collectLatest {
|
||||
_water.postValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the user weights
|
||||
viewModelScope.launch {
|
||||
_steps.postValue(stepRepository.todaySteps())
|
||||
}
|
||||
|
||||
// fetch the user weights
|
||||
viewModelScope.launch {
|
||||
weightRepository.getWeights().collectLatest {
|
||||
_weights.postValue(it)
|
||||
@ -70,24 +106,6 @@ class HomeViewModel @Inject internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
fun fetchWeights() = weightRepository.getWeights()
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
fun lastWeight() = weightRepository.lastWeight()
|
||||
|
||||
fun fetchWeight(id: Long) = weightRepository.getWeight(id)
|
||||
|
||||
suspend fun deleteWeight(weight: Weight) = weightRepository.deleteWeight(weight)
|
||||
|
||||
suspend fun addWeight(weight: Weight) = weightRepository.addWeight(weight)
|
||||
|
||||
fun fetchTodayWater() = waterRepository.todayWater()
|
||||
|
||||
fun updateWater(water: Water) {
|
||||
viewModelScope.launch {
|
||||
waterRepository.addWater(water)
|
||||
|
@ -40,7 +40,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
// Force only numbers on Goal
|
||||
val weightGoal = findPreference<EditTextPreference>("tmp_goal_weight")
|
||||
weightGoal?.apply {
|
||||
|
||||
setOnBindEditTextListener {
|
||||
it.setSelectAllOnFocus(true)
|
||||
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||
@ -60,7 +59,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val unit = config.getString(Settings.MASS_UNIT).value
|
||||
var modifier = Units.Mass.KILOGRAM.modifier
|
||||
|
@ -1,23 +1,29 @@
|
||||
package com.dzeio.openhealth.ui.steps
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.charts.Entry
|
||||
import com.dzeio.charts.axis.Line
|
||||
import com.dzeio.charts.series.BarSerie
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.adapters.StepsAdapter
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.data.step.Step
|
||||
import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.dzeio.openhealth.utils.ChartUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class StepsHomeFragment :
|
||||
@ -27,6 +33,8 @@ class StepsHomeFragment :
|
||||
const val TAG = "${Application.TAG}/SHFragment"
|
||||
}
|
||||
|
||||
private val args: StepsHomeFragmentArgs by navArgs()
|
||||
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentStepsHomeBinding =
|
||||
FragmentStepsHomeBinding::inflate
|
||||
|
||||
@ -35,102 +43,142 @@ class StepsHomeFragment :
|
||||
|
||||
viewModel.init()
|
||||
|
||||
val isDay = args.day > 0L
|
||||
|
||||
val recycler = binding.list
|
||||
|
||||
val manager = LinearLayoutManager(requireContext())
|
||||
recycler.layoutManager = manager
|
||||
|
||||
val adapter = StepsAdapter()
|
||||
adapter.onItemClick = {
|
||||
// findNavController().navigate(
|
||||
// WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterEdit(
|
||||
// it.id
|
||||
// )
|
||||
// )
|
||||
val adapter = StepsAdapter().apply {
|
||||
this.isDay = isDay
|
||||
}
|
||||
|
||||
if (!isDay) {
|
||||
adapter.onItemClick = {
|
||||
findNavController().navigate(
|
||||
StepsHomeFragmentDirections.actionNavStepsHomeSelf().apply {
|
||||
day = it.timestamp
|
||||
title = "Steps from " + it.formatTimestamp(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
recycler.adapter = adapter
|
||||
|
||||
val chart = binding.chart
|
||||
|
||||
// setup serie
|
||||
val serie = BarSerie(chart).apply {
|
||||
barPaint.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
)
|
||||
textPaint.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimary
|
||||
)
|
||||
}
|
||||
val serie = BarSerie(chart)
|
||||
|
||||
chart.apply {
|
||||
series = arrayListOf(serie)
|
||||
// debug = true
|
||||
|
||||
ChartUtils.materielTheme(chart, requireView())
|
||||
yAxis.apply {
|
||||
setYMax(500f)
|
||||
textLabel.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimaryContainer
|
||||
)
|
||||
linePaint.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimaryContainer
|
||||
)
|
||||
//
|
||||
onValueFormat = { value -> "${value.toInt()}" }
|
||||
setYMin(0f)
|
||||
}
|
||||
|
||||
xAxis.apply {
|
||||
increment = 3600000.0
|
||||
// displayCount = 168
|
||||
displayCount = 10
|
||||
textPaint.color = MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnPrimaryContainer
|
||||
)
|
||||
dataWidth = if (isDay) 8.64e+7 else 6.048e+8
|
||||
scrollEnabled = !isDay
|
||||
textPaint.textSize = 32f
|
||||
onValueFormat = onValueFormat@{
|
||||
val formatter = DateFormat.getDateTimeInstance(
|
||||
DateFormat.SHORT,
|
||||
val formatter = if (isDay) {
|
||||
DateFormat.getTimeInstance(
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
} else {
|
||||
DateFormat.getDateInstance(
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
}
|
||||
return@onValueFormat formatter.format(Date(it.toLong()))
|
||||
}
|
||||
}
|
||||
annotator.annotationTitleFormat = { "${it.y.roundToInt()} steps" }
|
||||
annotator.annotationSubTitleFormat = annotationSubTitleFormat@{
|
||||
val formatter = if (isDay) {
|
||||
DateFormat.getTimeInstance(
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
} else {
|
||||
DateFormat.getDateInstance(
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault()
|
||||
)
|
||||
return@onValueFormat formatter.format(Date(it.toLong()))
|
||||
}
|
||||
return@annotationSubTitleFormat formatter.format(Date(it.x.toLong()))
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.goal.observe(viewLifecycleOwner) {
|
||||
if (it != null && !isDay) {
|
||||
chart.yAxis.addLine(
|
||||
it.toFloat(),
|
||||
Line(true, Paint(chart.yAxis.linePaint).apply { strokeWidth = 4f })
|
||||
)
|
||||
chart.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.items.observe(viewLifecycleOwner) { list ->
|
||||
adapter.set(list)
|
||||
|
||||
if (list.isEmpty()) {
|
||||
adapter.set(arrayListOf())
|
||||
return@observe
|
||||
}
|
||||
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
val filtered = if (!isDay) {
|
||||
list
|
||||
} else {
|
||||
list.filter {
|
||||
it.getDay() == args.day
|
||||
}
|
||||
}
|
||||
if (isDay) {
|
||||
adapter.set(filtered)
|
||||
serie.entries = filtered.map {
|
||||
Entry(
|
||||
it.timestamp.toDouble(),
|
||||
it.value.toFloat()
|
||||
)
|
||||
} as ArrayList<Entry>
|
||||
} else {
|
||||
val entries: HashMap<Long, Entry> = HashMap()
|
||||
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
list.forEach {
|
||||
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
cal.timeInMillis = it.timestamp
|
||||
|
||||
// chart.animation.enabled = false
|
||||
// chart.animation.refreshRate = 60
|
||||
// chart.animation.duration = 300
|
||||
cal.set(Calendar.HOUR, 0)
|
||||
cal.set(Calendar.AM_PM, Calendar.AM)
|
||||
val ts = cal.timeInMillis
|
||||
if (!entries.containsKey(ts)) {
|
||||
entries[ts] = Entry((ts).toDouble(), 0F, chart.yAxis.goalLinePaint.color)
|
||||
}
|
||||
|
||||
// chart.scroller.zoomEnabled = false
|
||||
entries[ts]!!.y += it.value.toFloat()
|
||||
|
||||
// chart.xAxis.labels.size = 32f
|
||||
if (viewModel.goal.value != null) {
|
||||
if (entries[ts]!!.y > viewModel.goal.value!!) {
|
||||
entries[ts]!!.color = null
|
||||
}
|
||||
} else {
|
||||
entries[ts]!!.color = null
|
||||
}
|
||||
}
|
||||
|
||||
serie.entries = list.reversed().map {
|
||||
return@map Entry(it.timestamp.toDouble(), it.value.toFloat())
|
||||
} as ArrayList<Entry>
|
||||
adapter.set(
|
||||
entries.map { Step(value = it.value.y.toInt(), timestamp = it.key) }
|
||||
.sortedByDescending { it.timestamp }
|
||||
)
|
||||
|
||||
chart.xAxis.x = serie.entries.first().x
|
||||
serie.entries = ArrayList(entries.values)
|
||||
}
|
||||
|
||||
chart.xAxis.x =
|
||||
chart.xAxis.getXMax() - chart.xAxis.dataWidth!! + chart.xAxis.dataWidth!! / (if (isDay) 24 else 7)
|
||||
|
||||
chart.refresh()
|
||||
}
|
||||
|
@ -1,26 +1,37 @@
|
||||
package com.dzeio.openhealth.ui.steps
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dzeio.openhealth.Settings
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.step.Step
|
||||
import com.dzeio.openhealth.data.step.StepRepository
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StepsHomeViewModel@Inject internal constructor(
|
||||
private val stepRepository: StepRepository
|
||||
private val stepRepository: StepRepository,
|
||||
private val config: Configuration
|
||||
) : BaseViewModel() {
|
||||
val items: MutableLiveData<List<Step>> = MutableLiveData()
|
||||
|
||||
private val _goal: MutableLiveData<Int?> = MutableLiveData()
|
||||
val goal: LiveData<Int?> = _goal
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launch {
|
||||
stepRepository.getSteps().collectLatest {
|
||||
items.postValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
this._goal.postValue(
|
||||
config.getInt(Settings.STEPS_GOAL).value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.dzeio.openhealth.ui.tests
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.databinding.FragmentTestsBinding
|
||||
import com.dzeio.openhealth.utils.Bluetooth
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TestsFragment :
|
||||
BaseFragment<TestsViewModel, FragmentTestsBinding>(TestsViewModel::class.java) {
|
||||
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentTestsBinding
|
||||
get() = FragmentTestsBinding::inflate
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// Bindings
|
||||
binding.button.setOnClickListener {
|
||||
Bluetooth(requireContext()).apply {
|
||||
// scanDevices(cb)
|
||||
|
||||
// val device = DeviceMiSmartScale2(requireContext())
|
||||
//
|
||||
// device.status.addObserver {
|
||||
// Log.d("Device", "New device status $it")
|
||||
//
|
||||
// if (it == Device.ConnectionStatus.CONNECTED) {
|
||||
// device.fetchWeights().addObserver {
|
||||
// Log.i(
|
||||
// "FetchStatus",
|
||||
// "${(it.progress.toFloat() / it.progressMax.toFloat() * 100f).roundToInt()}% ${it.progress}/${it.progressMax}"
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// device.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.dzeio.openhealth.ui.tests
|
||||
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TestsViewModel @Inject internal constructor() : BaseViewModel()
|
@ -20,7 +20,7 @@ import com.google.android.material.datepicker.DateValidatorPointBackward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditWaterDialog :
|
||||
@ -31,8 +31,6 @@ class EditWaterDialog :
|
||||
|
||||
private val args: EditWaterDialogArgs by navArgs()
|
||||
|
||||
override val isFullscreenLayout = true
|
||||
|
||||
var newValue: Int = 0
|
||||
|
||||
override fun onDialogInit(dialog: Dialog) {
|
||||
@ -50,8 +48,11 @@ class EditWaterDialog :
|
||||
}
|
||||
binding.editTextNumber.doOnTextChanged { text, start, before, count ->
|
||||
val value = text.toString()
|
||||
newValue = if (value == "") 0
|
||||
else text.toString().toInt()
|
||||
newValue = if (value == "") {
|
||||
0
|
||||
} else {
|
||||
text.toString().toInt()
|
||||
}
|
||||
}
|
||||
|
||||
binding.date.setOnClickListener {
|
||||
@ -102,6 +103,7 @@ class EditWaterDialog :
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_fullscreen_dialog_save -> {
|
||||
|
@ -6,9 +6,9 @@ import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
import com.dzeio.openhealth.data.water.WaterRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class EditWaterViewModel @Inject internal constructor(
|
||||
@ -36,4 +36,4 @@ class EditWaterViewModel @Inject internal constructor(
|
||||
waterRepository.addWater(water)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,16 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.charts.Entry
|
||||
import com.dzeio.charts.series.BarSerie
|
||||
import com.dzeio.openhealth.adapters.WaterAdapter
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.databinding.FragmentMainWaterHomeBinding
|
||||
import com.dzeio.openhealth.utils.GraphUtils
|
||||
import com.github.mikephil.charting.data.BarData
|
||||
import com.github.mikephil.charting.data.BarDataSet
|
||||
import com.github.mikephil.charting.data.BarEntry
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.dzeio.openhealth.utils.ChartUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WaterHomeFragment :
|
||||
@ -45,39 +46,50 @@ class WaterHomeFragment :
|
||||
|
||||
val chart = binding.chart
|
||||
|
||||
GraphUtils.barChartSetup(
|
||||
chart,
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorPrimary
|
||||
),
|
||||
MaterialColors.getColor(
|
||||
requireView(),
|
||||
com.google.android.material.R.attr.colorOnBackground
|
||||
)
|
||||
)
|
||||
val serie = BarSerie(chart)
|
||||
|
||||
binding.buttonEditDefaultIntake.setOnClickListener {
|
||||
findNavController().navigate(WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterSizeDialog())
|
||||
chart.apply {
|
||||
ChartUtils.materielTheme(chart, requireView())
|
||||
|
||||
yAxis.apply {
|
||||
// onValueFormat
|
||||
}
|
||||
|
||||
xAxis.apply {
|
||||
dataWidth = 604800000.0
|
||||
textPaint.textSize = 32f
|
||||
onValueFormat = onValueFormat@{
|
||||
return@onValueFormat SimpleDateFormat(
|
||||
"yyyy-MM-dd",
|
||||
Locale.getDefault()
|
||||
).format(Date(it.toLong()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chart.xAxis.valueFormatter = GraphUtils.DateValueFormatter(1000 * 60 * 60 * 24)
|
||||
binding.buttonEditDefaultIntake.setOnClickListener {
|
||||
findNavController().navigate(
|
||||
WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterSizeDialog()
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.items.observe(viewLifecycleOwner) { list ->
|
||||
adapter.set(list)
|
||||
|
||||
val dataset = BarDataSet(
|
||||
list.map {
|
||||
return@map BarEntry(
|
||||
(it.timestamp / 1000 / 60 / 60 / 24).toFloat(),
|
||||
it.value.toFloat()
|
||||
)
|
||||
},
|
||||
""
|
||||
)
|
||||
if (list.isEmpty()) {
|
||||
return@observe
|
||||
}
|
||||
|
||||
chart.data = BarData(dataset)
|
||||
chart.invalidate()
|
||||
val dataset = list.map {
|
||||
return@map Entry(
|
||||
it.timestamp.toDouble(),
|
||||
it.value.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
serie.entries = dataset as ArrayList<Entry>
|
||||
chart.xAxis.x = dataset[0].x
|
||||
chart.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
import com.dzeio.openhealth.data.water.WaterRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class WaterHomeViewModel@Inject internal constructor(
|
||||
@ -23,4 +23,4 @@ class WaterHomeViewModel@Inject internal constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.dzeio.openhealth.ui.water
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.marginBottom
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseDialog
|
||||
import com.dzeio.openhealth.databinding.DialogWaterSizeSelectorBinding
|
||||
|
@ -15,24 +15,22 @@ import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseFullscreenDialog
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.databinding.DialogEditWeightBinding
|
||||
import com.dzeio.openhealth.ui.home.HomeViewModel
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditWeightDialog :
|
||||
BaseFullscreenDialog<HomeViewModel, DialogEditWeightBinding>(HomeViewModel::class.java) {
|
||||
BaseFullscreenDialog<EditWeightDialogViewModel, DialogEditWeightBinding>(
|
||||
EditWeightDialogViewModel::class.java
|
||||
) {
|
||||
|
||||
override val bindingInflater: (LayoutInflater) -> DialogEditWeightBinding =
|
||||
DialogEditWeightBinding::inflate
|
||||
|
||||
override val isFullscreenLayout = true
|
||||
|
||||
val args: EditWeightDialogArgs by navArgs()
|
||||
private val args: EditWeightDialogArgs by navArgs()
|
||||
|
||||
lateinit var weight: Weight
|
||||
|
||||
@ -101,10 +99,7 @@ class EditWeightDialog :
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < N")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
@ -144,6 +139,5 @@ class EditWeightDialog :
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user