mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-22 02:42:15 +00:00
feat: Add bluetooth Scales support (#144)
This commit is contained in:
parent
e14e3881c1
commit
1f780ae2c4
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
|
@ -163,6 +163,10 @@ android {
|
||||
namespace = appID
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Dzeio Charts
|
||||
implementation("com.dzeio:charts:fe20f90654")
|
||||
@ -198,6 +202,7 @@ dependencies {
|
||||
|
||||
// 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")
|
||||
|
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_outline_monitor_weight_24"
|
||||
android:background="@drawable/shape_circle"
|
||||
app:tint="?colorOnPrimary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="4dp"
|
||||
android:text="History" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.dzeio.charts.ChartView
|
||||
android:id="@+id/chart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:minHeight="200dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/goal_button"
|
||||
android:text="@string/add_goal"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/debug_random_values"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Generate random values" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:clipToPadding="false"
|
||||
android:id="@+id/list"
|
||||
android:padding="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
tools:listitem="@layout/layout_item_list"
|
||||
tools:context=".ui.weight.ListWeightFragment" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -9,6 +9,14 @@
|
||||
<!-- 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" />
|
||||
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:allowBackup="true"
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -24,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
|
||||
*
|
||||
|
@ -15,6 +15,9 @@ 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)
|
||||
|
||||
|
@ -2,6 +2,8 @@ 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
|
||||
@ -54,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,6 +1,7 @@
|
||||
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
|
||||
@ -25,8 +26,10 @@ import com.dzeio.openhealth.data.weight.WeightDao
|
||||
Step::class,
|
||||
Food::class
|
||||
],
|
||||
// TODO: go back to version 1 when the app is published
|
||||
version = 1,
|
||||
version = 2,
|
||||
autoMigrations = [
|
||||
AutoMigration(1, 2)
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
@ -69,7 +69,8 @@ data class Food(
|
||||
if (
|
||||
food.nutriments == null ||
|
||||
food.name == null ||
|
||||
((food.servingSize == null || food.servingSize == "") && (food.quantity == null || food.quantity == "") && food.servingQuantity == null && food.productQuantity == null)) {
|
||||
((food.servingSize == null || food.servingSize == "") && (food.quantity == null || food.quantity == "") && food.servingQuantity == null && food.productQuantity == null)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -93,12 +94,12 @@ data class Food(
|
||||
// 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,
|
||||
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(),
|
||||
energy = food.nutriments!!.energy
|
||||
?: (food.nutriments!!.energyKJ * 0.2390057361).toFloat(),
|
||||
image = food.image
|
||||
)
|
||||
}
|
||||
|
@ -18,5 +18,4 @@ interface FoodDao : BaseDao<Food> {
|
||||
|
||||
@Query("Select * FROM Food ORDER BY timestamp DESC LIMIT 1")
|
||||
fun last(): Flow<Food?>
|
||||
|
||||
}
|
||||
|
@ -2,12 +2,12 @@ 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
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FoodRepository @Inject constructor(
|
||||
private val dao: FoodDao,
|
||||
|
@ -43,7 +43,7 @@ data class OFFProduct(
|
||||
* the product nutriments
|
||||
*/
|
||||
@SerializedName("nutriments")
|
||||
var nutriments: OFFNutriments,
|
||||
var nutriments: OFFNutriments?,
|
||||
|
||||
/**
|
||||
* the product image
|
||||
|
@ -34,15 +34,20 @@ interface OpenFoodFactService {
|
||||
/**
|
||||
* 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")
|
||||
@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")
|
||||
@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>
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package com.dzeio.openhealth.data.step
|
||||
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
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(
|
||||
@ -46,5 +46,4 @@ class StepRepository @Inject constructor(
|
||||
fun currentStep() = lastStep().filter {
|
||||
return@filter it != null && it.isCurrent()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ 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
|
||||
@ -18,7 +19,7 @@ import kotlinx.coroutines.runBlocking
|
||||
* TODO: rewrite to use the new libs
|
||||
*/
|
||||
class StepSource(
|
||||
private val context: Context,
|
||||
context: Context,
|
||||
private val callback: ((Float) -> Unit)? = null
|
||||
) : SensorEventListener {
|
||||
|
||||
@ -33,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")
|
||||
}
|
||||
}
|
||||
@ -73,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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,65 @@ data class Weight(
|
||||
*
|
||||
* note: Unused currently but kept for future usage
|
||||
*/
|
||||
var source: String = "OpenHealth"
|
||||
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;
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -29,4 +30,8 @@ class SystemModule {
|
||||
fun provideConfig(sharedPreferences: SharedPreferences): Configuration {
|
||||
return Configuration(sharedPreferences)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideBluetooth(@ApplicationContext context: Context): Bluetooth = Bluetooth(context)
|
||||
}
|
||||
|
@ -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
|
||||
@ -101,7 +103,7 @@ class OpenHealthService : Service() {
|
||||
if (step != null) {
|
||||
step.value += stepsBuffer
|
||||
stepRepository.updateStep(step)
|
||||
// create a new steps object and send it
|
||||
// create a new steps object and send it
|
||||
} else {
|
||||
stepRepository.addStep(Step(value = stepsBuffer))
|
||||
}
|
||||
@ -119,7 +121,7 @@ class OpenHealthService : Service() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopForeground(true)
|
||||
stopForegroundPoly(NotificationBehavior.REMOVE)
|
||||
|
||||
// Tell the user we stopped.
|
||||
Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show()
|
||||
|
@ -116,7 +116,6 @@ class BrowseFragment :
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateStepsText(numberOfSteps: Int?, goal: Int?) {
|
||||
|
@ -9,15 +9,15 @@ 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,
|
||||
private val config: Configuration
|
||||
config: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _steps = MutableLiveData(0)
|
||||
@ -41,6 +41,5 @@ class BrowseViewModel @Inject internal constructor(
|
||||
_weight.postValue(it.weight)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ class FoodDialog :
|
||||
|
||||
private var quantity: Float? = null
|
||||
|
||||
|
||||
override val bindingInflater: (LayoutInflater) -> DialogFoodProductBinding =
|
||||
DialogFoodProductBinding::inflate
|
||||
|
||||
@ -50,7 +49,6 @@ class FoodDialog :
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreated() {
|
||||
super.onCreated()
|
||||
Log.d("FoodDialog", args.id.toString())
|
||||
|
@ -6,9 +6,9 @@ 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
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FoodDialogViewModel @Inject internal constructor(
|
||||
@ -41,7 +41,7 @@ class FoodDialogViewModel @Inject internal constructor(
|
||||
return
|
||||
}
|
||||
|
||||
val transformer = (quantity ?: it.quantity) / it.quantity
|
||||
val transformer = quantity / it.quantity
|
||||
it.energy = it.energy * transformer
|
||||
it.proteins = it.proteins * transformer
|
||||
it.carbohydrates = it.carbohydrates * transformer
|
||||
|
@ -64,7 +64,6 @@ class FoodHomeFragment :
|
||||
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()
|
||||
@ -87,7 +86,9 @@ class FoodHomeFragment :
|
||||
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)}"
|
||||
binding.date.text = "${date.get(Calendar.YEAR)}-${date.get(Calendar.MONTH) + 1}-${date.get(
|
||||
Calendar.DAY_OF_MONTH
|
||||
)}"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,17 +5,15 @@ 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.data.openfoodfact.OpenFoodFactService
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
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,
|
||||
private val foodFactService: OpenFoodFactService
|
||||
private val foodRepository: FoodRepository
|
||||
) : BaseViewModel() {
|
||||
val items: MutableLiveData<List<Food>> = MutableLiveData()
|
||||
private val list: MutableLiveData<List<Food>> = MutableLiveData(arrayListOf())
|
||||
@ -62,8 +60,10 @@ class FoodHomeViewModel @Inject internal constructor(
|
||||
day.add(Calendar.DAY_OF_YEAR, 1)
|
||||
val tomorrow = day.timeInMillis
|
||||
|
||||
items.postValue((foods ?: list.value!!).filter { food ->
|
||||
food.timestamp in todayInMillis until tomorrow
|
||||
})
|
||||
items.postValue(
|
||||
(foods ?: list.value!!).filter { food ->
|
||||
food.timestamp in todayInMillis until tomorrow
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ 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
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchFoodDialogViewModel @Inject internal constructor(
|
||||
|
@ -7,12 +7,14 @@ 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
|
||||
@ -98,6 +100,17 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
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) {
|
||||
@ -218,7 +231,7 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
animator.duration = 300 // ms
|
||||
animator.addUpdateListener {
|
||||
this.oldValue = (it.animatedValue as Int).toFloat()
|
||||
val value = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat()
|
||||
val value = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake
|
||||
|
||||
val graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(graph)
|
||||
|
@ -13,9 +13,9 @@ import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
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(
|
||||
|
@ -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
|
||||
@ -91,10 +89,5 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
return@setOnPreferenceChangeListener true
|
||||
}
|
||||
|
||||
val stepsGoalPreference = findPreference<EditTextPreference>(Settings.STEPS_GOAL)
|
||||
stepsGoalPreference.apply {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,8 +129,12 @@ class StepsHomeFragment :
|
||||
return@observe
|
||||
}
|
||||
|
||||
val filtered = if (!isDay) list else list.filter {
|
||||
it.getDay() == args.day
|
||||
val filtered = if (!isDay) {
|
||||
list
|
||||
} else {
|
||||
list.filter {
|
||||
it.getDay() == args.day
|
||||
}
|
||||
}
|
||||
if (isDay) {
|
||||
adapter.set(filtered)
|
||||
|
@ -9,9 +9,9 @@ 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(
|
||||
|
@ -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()
|
@ -48,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 {
|
||||
@ -100,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,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
|
||||
|
@ -23,7 +23,9 @@ import java.util.Date
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditWeightDialog :
|
||||
BaseFullscreenDialog<EditWeightDialogViewModel, DialogEditWeightBinding>(EditWeightDialogViewModel::class.java) {
|
||||
BaseFullscreenDialog<EditWeightDialogViewModel, DialogEditWeightBinding>(
|
||||
EditWeightDialogViewModel::class.java
|
||||
) {
|
||||
|
||||
override val bindingInflater: (LayoutInflater) -> DialogEditWeightBinding =
|
||||
DialogEditWeightBinding::inflate
|
||||
@ -97,10 +99,7 @@ class EditWeightDialog :
|
||||
} else {
|
||||
TODO("VERSION.SDK_INT < N")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
@ -140,6 +139,5 @@ class EditWeightDialog :
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
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 EditWeightDialogViewModel @Inject internal constructor(
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.dzeio.openhealth.ui.weight
|
||||
|
||||
import android.Manifest
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Paint
|
||||
import android.os.Bundle
|
||||
@ -9,6 +10,8 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
@ -16,11 +19,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.R
|
||||
import com.dzeio.openhealth.adapters.WeightAdapter
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.databinding.FragmentListWeightBinding
|
||||
import com.dzeio.openhealth.utils.PermissionsManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.DateFormat
|
||||
@ -34,6 +39,40 @@ class ListWeightFragment :
|
||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) ->
|
||||
FragmentListWeightBinding = FragmentListWeightBinding::inflate
|
||||
|
||||
private lateinit var button: View
|
||||
private val activityResult = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) {
|
||||
if (it.containsValue(false)) {
|
||||
// TODO: Show a popup with choice to change it
|
||||
Toast.makeText(requireContext(), R.string.permission_declined, Toast.LENGTH_LONG).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
button.callOnClick()
|
||||
}
|
||||
|
||||
private val menuProvider = object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menu.findItem(R.id.action_add).isVisible = true
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_add -> {
|
||||
findNavController().navigate(
|
||||
ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(
|
||||
WeightDialog.DialogTypes.ADD_WEIGHT.ordinal
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val settings: SharedPreferences by lazy {
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
}
|
||||
@ -42,26 +81,7 @@ class ListWeightFragment :
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Menu
|
||||
requireActivity().addMenuProvider(object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menu.findItem(R.id.action_add).isVisible = true
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_add -> {
|
||||
findNavController().navigate(
|
||||
ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(
|
||||
WeightDialog.DialogTypes.ADD_WEIGHT.ordinal
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
})
|
||||
requireActivity().addMenuProvider(menuProvider)
|
||||
|
||||
if (viewModel.goalWeight.value != null) {
|
||||
binding.goalButton.setText(R.string.edit_goal)
|
||||
@ -75,6 +95,24 @@ class ListWeightFragment :
|
||||
)
|
||||
}
|
||||
|
||||
binding.bluetoothButton.setOnClickListener {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
val hasPermission = PermissionsManager.hasPermission(requireContext(), permissions)
|
||||
if (!hasPermission) {
|
||||
button = binding.bluetoothButton
|
||||
activityResult.launch(permissions)
|
||||
return@setOnClickListener
|
||||
}
|
||||
findNavController().navigate(
|
||||
ListWeightFragmentDirections.actionNavListWeightToScanScalesDialog()
|
||||
)
|
||||
}
|
||||
|
||||
val adapter = WeightAdapter().apply {
|
||||
onItemClick = {
|
||||
findNavController().navigate(
|
||||
@ -152,15 +190,21 @@ class ListWeightFragment :
|
||||
}
|
||||
|
||||
// Debug button
|
||||
// if (binding.debugRandomValues != null) {
|
||||
// binding.debugRandomValues.setOnClickListener {
|
||||
// viewModel.generateRandomValues()
|
||||
// }
|
||||
// binding.debugRandomValues.setOnLongClickListener {
|
||||
// viewModel.delete(viewModel.weights.value!!)
|
||||
// return@setOnLongClickListener true
|
||||
// }
|
||||
// }
|
||||
if (BuildConfig.DEBUG) {
|
||||
binding.debugRandomValues.visibility = View.VISIBLE
|
||||
binding.debugRandomValues.setOnClickListener {
|
||||
viewModel.generateRandomValues()
|
||||
}
|
||||
binding.debugRandomValues.setOnLongClickListener {
|
||||
viewModel.delete(viewModel.weights.value!!)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
requireActivity().removeMenuProvider(menuProvider)
|
||||
}
|
||||
|
||||
private fun updateGraph(list: List<Weight>) {
|
||||
|
@ -10,15 +10,15 @@ import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
import com.dzeio.openhealth.units.Units
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class ListWeightViewModel @Inject internal constructor(
|
||||
private val weightRepository: WeightRepository,
|
||||
private val settings: Configuration
|
||||
settings: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _massUnit = MutableLiveData(Units.Mass.KILOGRAM)
|
||||
@ -60,7 +60,7 @@ class ListWeightViewModel @Inject internal constructor(
|
||||
|
||||
fun delete(list: List<Weight>) {
|
||||
viewModelScope.launch {
|
||||
for (item in list) weightRepository.deleteWeight(item)
|
||||
weightRepository.deleteWeight(*list.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,111 @@
|
||||
package com.dzeio.openhealth.ui.weight
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ProgressDialog
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.adapters.ItemAdapter
|
||||
import com.dzeio.openhealth.core.BaseDialog
|
||||
import com.dzeio.openhealth.databinding.DialogSearchBinding
|
||||
import com.dzeio.openhealth.devices.BluetoothLeGattDevice
|
||||
import com.dzeio.openhealth.devices.Device
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScanScalesDialog :
|
||||
BaseDialog<ScanScalesViewModel, DialogSearchBinding>(ScanScalesViewModel::class.java) {
|
||||
|
||||
override val bindingInflater: (LayoutInflater) -> DialogSearchBinding =
|
||||
DialogSearchBinding::inflate
|
||||
|
||||
override fun onBuilderInit(builder: MaterialAlertDialogBuilder) {
|
||||
super.onBuilderInit(builder)
|
||||
|
||||
builder.apply {
|
||||
setTitle(R.string.searching_scales)
|
||||
setIcon(R.drawable.ic_outline_monitor_weight_24)
|
||||
setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
viewModel.stopScan()
|
||||
dialog.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onCreated() {
|
||||
super.onCreated()
|
||||
binding.search.visibility = View.GONE
|
||||
binding.loading.visibility = View.VISIBLE
|
||||
|
||||
val adapter = ItemAdapter<BluetoothLeGattDevice>().apply {
|
||||
onItemClick = { deviceAdapter ->
|
||||
dialog!!.dismiss()
|
||||
val device = deviceAdapter.value
|
||||
viewModel.stopScan()
|
||||
val progress = ProgressDialog(requireContext()).apply {
|
||||
setTitle("Connecting & Fetching...")
|
||||
setCancelable(false)
|
||||
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
|
||||
show()
|
||||
}
|
||||
device.connect().addObserver { status ->
|
||||
if (status === Device.ConnectionStatus.CONNECTED) {
|
||||
device.fetchWeights().addObserver {
|
||||
progress.progress = it.progress
|
||||
progress.max = it.progressMax + 1
|
||||
|
||||
if (it.progress == it.progressMax) {
|
||||
device.close()
|
||||
Log.d("YAY", "${it.data}")
|
||||
lifecycleScope.launch {
|
||||
viewModel.addWeights(it.data)
|
||||
progress.dismiss()
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Data synchonised with the remote device",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (status === Device.ConnectionStatus.ERROR) {
|
||||
progress.dismiss()
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"An error occured while connecting...",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.list.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
this.adapter = adapter
|
||||
}
|
||||
|
||||
viewModel.devices.observe(this) {
|
||||
if (it == null) {
|
||||
adapter.clear()
|
||||
return@observe
|
||||
}
|
||||
adapter.set(
|
||||
it.map {
|
||||
ItemAdapter.Item(
|
||||
it,
|
||||
it.name,
|
||||
"${it.item!!.name} (${it.item!!.address})",
|
||||
icon = R.drawable.ic_baseline_add_24
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.dzeio.openhealth.ui.weight
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.dzeio.openhealth.core.BaseViewModel
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
import com.dzeio.openhealth.devices.BluetoothLeGattDevice
|
||||
import com.dzeio.openhealth.utils.Bluetooth
|
||||
import com.dzeio.openhealth.utils.Configuration
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
@HiltViewModel
|
||||
class ScanScalesViewModel @Inject internal constructor(
|
||||
private val bluetooth: Bluetooth,
|
||||
private val weightRepository: WeightRepository,
|
||||
config: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_SCAN")
|
||||
val devices = BluetoothLeGattDevice.findDevices(bluetooth, config).toLiveData()
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_SCAN")
|
||||
fun stopScan() {
|
||||
bluetooth.stopScan()
|
||||
}
|
||||
|
||||
suspend fun addWeights(weights: ArrayList<Weight>) {
|
||||
val current = weightRepository.getWeights().first()
|
||||
val toAdd = arrayListOf<Weight>()
|
||||
for (weight in weights) {
|
||||
if (current.find { it.equals(current) } != null) {
|
||||
continue
|
||||
}
|
||||
toAdd.add(weight)
|
||||
}
|
||||
weightRepository.addAll(*toAdd.toTypedArray())
|
||||
}
|
||||
}
|
@ -66,7 +66,6 @@ class WeightDialog :
|
||||
|
||||
binding.gram.maxValue = 9
|
||||
binding.gram.minValue = 0
|
||||
|
||||
}
|
||||
|
||||
private fun setValue(value: Float) {
|
||||
|
@ -10,14 +10,14 @@ import com.dzeio.openhealth.data.weight.WeightRepository
|
||||
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 WeightDialogViewModel @Inject internal constructor(
|
||||
private val weightRepository: WeightRepository,
|
||||
private val settings: Configuration
|
||||
settings: Configuration
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _goalWeight = settings.getFloat(Settings.WEIGHT_GOAL)
|
||||
@ -40,7 +40,6 @@ class WeightDialogViewModel @Inject internal constructor(
|
||||
weightRepository.lastWeight().collectLatest {
|
||||
_weight.postValue(it)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ object Units {
|
||||
it.id == value
|
||||
} ?: KILOGRAM
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun format(value: Float): Float {
|
||||
|
148
app/src/main/java/com/dzeio/openhealth/utils/Bluetooth.kt
Normal file
148
app/src/main/java/com/dzeio/openhealth/utils/Bluetooth.kt
Normal file
@ -0,0 +1,148 @@
|
||||
package com.dzeio.openhealth.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.dzeio.openhealth.utils.polyfills.getBluetoothDevice
|
||||
|
||||
class Bluetooth(
|
||||
private val context: Context
|
||||
) {
|
||||
companion object {
|
||||
const val TAG = "Bluetooth"
|
||||
}
|
||||
|
||||
private lateinit var adapter: BluetoothAdapter
|
||||
|
||||
init {
|
||||
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {
|
||||
throw Exception("Phone missing the Bluetooth feature")
|
||||
}
|
||||
try {
|
||||
adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.message ?: "error getting default adapter")
|
||||
}
|
||||
if (!adapter.isEnabled) {
|
||||
Toast.makeText(context, "Bluetooth is not enabled", Toast.LENGTH_LONG).show()
|
||||
Log.e(TAG, "Bluetooth is not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
fun getAddress(): String {
|
||||
return adapter.address
|
||||
}
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
|
||||
fun getName(): String {
|
||||
return adapter.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the current phone discoverable through bluetooth scan
|
||||
*/
|
||||
// fun makeDeviceDiscoverable(time: Int = 600) {
|
||||
// val intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
|
||||
// putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, time)
|
||||
// }
|
||||
// activity.startActivityForResult(intent, 1)
|
||||
// }
|
||||
|
||||
fun getDiscoverableIntent(time: Int = 600): Intent {
|
||||
return Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
|
||||
putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, time)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var cb: (device: BluetoothDevice) -> Boolean
|
||||
|
||||
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
@RequiresPermission(allOf = ["android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"])
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
if (BluetoothDevice.ACTION_FOUND == action) {
|
||||
val device =
|
||||
intent.getBluetoothDevice()
|
||||
// val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE).toInt()
|
||||
|
||||
if (device != null) {
|
||||
Log.d(TAG, "Device found! (${device.name ?: device.address})")
|
||||
val end = cb(device)
|
||||
if (end) {
|
||||
stopScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback return boolean if true it stop scan
|
||||
*/
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_SCAN")
|
||||
fun scanDevices(callback: (device: BluetoothDevice) -> Boolean): Boolean {
|
||||
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
|
||||
context.registerReceiver(receiver, filter)
|
||||
cb = callback
|
||||
return adapter.startDiscovery()
|
||||
}
|
||||
|
||||
private var leCallback: ((device: BluetoothDevice) -> Boolean)? = null
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
@RequiresPermission(allOf = ["android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT"])
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult?) {
|
||||
super.onScanResult(callbackType, result)
|
||||
if (result != null) {
|
||||
val device = result.device
|
||||
Log.d(TAG, "Device found! (${device.name ?: device.address})")
|
||||
val doStop = leCallback?.invoke(device)
|
||||
if (doStop == true) {
|
||||
stopScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback return boolean if true it stop scan
|
||||
*/
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_SCAN")
|
||||
fun scanLeDevices(callback: (device: BluetoothDevice) -> Boolean) {
|
||||
val pouet = adapter.bluetoothLeScanner
|
||||
leCallback = callback
|
||||
pouet.startScan(scanCallback)
|
||||
}
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_SCAN")
|
||||
fun stopScan() {
|
||||
adapter.bluetoothLeScanner.stopScan(scanCallback)
|
||||
adapter.cancelDiscovery()
|
||||
try {
|
||||
context.unregisterReceiver(receiver)
|
||||
} catch (it: IllegalArgumentException) {
|
||||
Log.i(TAG, "Seems like it was already unloaded", it)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connectGatt(
|
||||
device: BluetoothDevice,
|
||||
autoConnect: Boolean,
|
||||
callback: BluetoothGattCallback
|
||||
) {
|
||||
device.connectGatt(context, autoConnect, callback)
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package com.dzeio.openhealth.utils
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.core.Observable
|
||||
@ -27,66 +26,66 @@ class Configuration(
|
||||
|
||||
fun getString(key: String): StringField {
|
||||
if (cache[key] == null) {
|
||||
Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
// Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
cache[key] = StringField(key)
|
||||
} else {
|
||||
Log.d(TAG, "$key in cache")
|
||||
// Log.d(TAG, "$key in cache")
|
||||
}
|
||||
return cache[key] as StringField
|
||||
}
|
||||
|
||||
fun getLong(key: String): LongField {
|
||||
if (cache[key] == null) {
|
||||
Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
// Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
cache[key] = LongField(key)
|
||||
} else {
|
||||
Log.d(TAG, "$key in cache")
|
||||
// Log.d(TAG, "$key in cache")
|
||||
}
|
||||
return cache[key] as LongField
|
||||
}
|
||||
|
||||
fun getBoolean(key: String): BooleanField {
|
||||
if (cache[key] == null) {
|
||||
Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
// Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
cache[key] = BooleanField(key)
|
||||
} else {
|
||||
Log.d(TAG, "$key in cache")
|
||||
// Log.d(TAG, "$key in cache")
|
||||
}
|
||||
return cache[key] as BooleanField
|
||||
}
|
||||
|
||||
fun getInt(key: String): IntField {
|
||||
if (cache[key] == null) {
|
||||
Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
// Log.d(TAG, "$key is not cache, creating new instance")
|
||||
cache[key] = IntField(key)
|
||||
} else {
|
||||
Log.d(TAG, "$key in cache")
|
||||
// Log.d(TAG, "$key in cache")
|
||||
}
|
||||
return cache[key] as IntField
|
||||
}
|
||||
|
||||
fun getFloat(key: String): FloatField {
|
||||
if (cache[key] == null) {
|
||||
Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
// Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
cache[key] = FloatField(key)
|
||||
} else {
|
||||
Log.d(TAG, "$key in cache")
|
||||
// Log.d(TAG, "$key in cache")
|
||||
}
|
||||
return cache[key] as FloatField
|
||||
}
|
||||
|
||||
fun getStringSet(key: String): StringSetField {
|
||||
if (cache[key] == null) {
|
||||
Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
// Log.d(TAG, "$key does not exist in cache, creating new instance")
|
||||
cache[key] = StringSetField(key)
|
||||
} else {
|
||||
Log.d(TAG, "$key in cache")
|
||||
// Log.d(TAG, "$key in cache")
|
||||
}
|
||||
return cache[key] as StringSetField
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(u: SharedPreferences, key: String) {
|
||||
Log.d(TAG, "configuration update for key: $key")
|
||||
// Log.d(TAG, "configuration update for key: $key")
|
||||
cache[key]?.needUpdate = true
|
||||
cache[key]?.notifyObservers()
|
||||
}
|
||||
|
@ -38,5 +38,4 @@ object DrawUtils {
|
||||
val it = (if (isWidth) this.width else this.height) * multiplier
|
||||
return it * value / 100
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,11 +9,10 @@ import androidx.preference.PreferenceManager
|
||||
import com.dzeio.openhealth.Settings
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
/**
|
||||
* Utils object for [Locale]
|
||||
*
|
||||
* @see https://github.com/gunhansancar/ChangeLanguageExample/blob/master/app/src/main/java/com/gunhansancar/changelanguageexample/helper/LocaleHelper.java
|
||||
* https://github.com/gunhansancar/ChangeLanguageExample/blob/master/app/src/main/java/com/gunhansancar/changelanguageexample/helper/LocaleHelper.java
|
||||
*/
|
||||
object LocaleUtils {
|
||||
fun onAttach(context: Context): Context {
|
||||
@ -68,8 +67,10 @@ object LocaleUtils {
|
||||
Locale.setDefault(locale)
|
||||
val resources = context.resources
|
||||
val configuration: Configuration = resources.configuration
|
||||
@Suppress("DEPRECATION")
|
||||
configuration.locale = locale
|
||||
configuration.setLayoutDirection(locale)
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(configuration, resources.displayMetrics)
|
||||
return context
|
||||
}
|
||||
|
@ -36,4 +36,3 @@ object PermissionsManager {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.text.InputType
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
import androidx.preference.EditTextPreference
|
||||
|
||||
@ -48,7 +47,6 @@ class IntEditTextPreference : EditTextPreference, EditTextPreference.OnBindEditT
|
||||
* @param text The text to save
|
||||
*/
|
||||
override fun setText(text: String?) {
|
||||
|
||||
val wasBlocking = shouldDisableDependents()
|
||||
val pouet = Integer.parseInt(text.toString())
|
||||
this.txt = text
|
||||
|
@ -0,0 +1,45 @@
|
||||
package com.dzeio.openhealth.utils.polyfills
|
||||
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresPermission
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
|
||||
fun BluetoothGatt.writeCharacteristicPoly(
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray,
|
||||
writeType: Int
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
this.writeCharacteristic(
|
||||
characteristic,
|
||||
value,
|
||||
writeType
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
characteristic.value = value
|
||||
@Suppress("DEPRECATION")
|
||||
this.writeCharacteristic(characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
|
||||
fun BluetoothGatt.writeDescriptorPoly(
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
value: ByteArray
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
this.writeDescriptor(
|
||||
descriptor,
|
||||
value
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
descriptor.value = value
|
||||
@Suppress("DEPRECATION")
|
||||
this.writeDescriptor(descriptor)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.dzeio.openhealth.utils.polyfills
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
|
||||
fun Intent.getBluetoothDevice(): BluetoothDevice? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.dzeio.openhealth.utils.polyfills
|
||||
|
||||
import android.app.Service
|
||||
import android.os.Build
|
||||
|
||||
enum class NotificationBehavior {
|
||||
REMOVE,
|
||||
DETACH
|
||||
}
|
||||
|
||||
fun Service.stopForegroundPoly(notificationBehavior: NotificationBehavior) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(
|
||||
if (notificationBehavior === NotificationBehavior.REMOVE) {
|
||||
Service.STOP_FOREGROUND_REMOVE
|
||||
} else {
|
||||
Service.STOP_FOREGROUND_DETACH
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(notificationBehavior == NotificationBehavior.REMOVE)
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/ic_bluetooth.xml
Normal file
5
app/src/main/res/drawable/ic_bluetooth.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal" 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="M17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88z"/>
|
||||
</vector>
|
39
app/src/main/res/layout/dialog_search.xml
Normal file
39
app/src/main/res/layout/dialog_search.xml
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:nestedScrollingEnabled="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:text="@string/connectivity_error" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/item_list">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -7,6 +7,13 @@
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
|
||||
<Button
|
||||
android:id="@+id/goto_tests"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:text="Goto Tests" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -306,7 +313,7 @@
|
||||
<com.dzeio.charts.ChartView
|
||||
android:id="@+id/weight_graph"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_margin="8dp"
|
||||
android:minHeight="200dp" />
|
||||
|
||||
|
@ -6,6 +6,62 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:paddingVertical="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_bluetooth"
|
||||
android:background="@drawable/shape_circle"
|
||||
app:tint="?colorOnPrimary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="4dp"
|
||||
android:text="Bluetooth Scale Status" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/bluetooth_button"
|
||||
android:text="@string/add_goal"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginVertical="16dp"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="match_parent"
|
||||
@ -74,6 +130,14 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/debug_random_values"
|
||||
android:layout_width="match_parent"
|
||||
android:visibility="gone"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Generate random values" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:clipToPadding="false"
|
||||
android:id="@+id/list"
|
||||
|
15
app/src/main/res/layout/fragment_tests.xml
Normal file
15
app/src/main/res/layout/fragment_tests.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.home.HomeFragment">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Button" />
|
||||
</LinearLayout>
|
62
app/src/main/res/layout/item_list.xml
Normal file
62
app/src/main/res/layout/item_list.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/card"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="1">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="43dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Title"
|
||||
android:text="" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sub_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:text="Description"
|
||||
android:text="" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_right"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_baseline_edit_24" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
@ -27,6 +27,9 @@
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_nav_weight_dialog"
|
||||
app:destination="@id/nav_weight_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_nav_home_to_testsFragment"
|
||||
app:destination="@id/testsFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
@ -43,6 +46,9 @@
|
||||
<action
|
||||
android:id="@+id/action_nav_list_weight_to_nav_weight_dialog"
|
||||
app:destination="@id/nav_weight_dialog" />
|
||||
<action
|
||||
android:id="@+id/action_nav_list_weight_to_scanScalesDialog"
|
||||
app:destination="@id/scanScalesDialog" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
@ -196,4 +202,14 @@
|
||||
app:argType="boolean"
|
||||
/>
|
||||
</dialog>
|
||||
<fragment
|
||||
android:id="@+id/testsFragment"
|
||||
android:name="com.dzeio.openhealth.ui.tests.TestsFragment"
|
||||
tools:layout="@layout/fragment_tests"
|
||||
android:label="TestsFragment" />
|
||||
<dialog
|
||||
android:id="@+id/scanScalesDialog"
|
||||
tools:layout="@layout/dialog_search"
|
||||
android:name="com.dzeio.openhealth.ui.weight.ScanScalesDialog"
|
||||
android:label="ScanScalesDialog" />
|
||||
</navigation>
|
||||
|
@ -68,4 +68,5 @@
|
||||
<string name="food_description" translatable="false">%1$s (%2$.0f kcal)</string>
|
||||
<string name="steps_count">%1$d steps</string>
|
||||
<string name="connectivity_error">It seems that we can\'t communicate with OpenFoodFact, please retry later</string>
|
||||
<string name="searching_scales">Searchin Scales</string>
|
||||
</resources>
|
||||
|
@ -1,9 +1,8 @@
|
||||
package com.dzeio.openhealth
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
@ -14,4 +13,4 @@ class ExampleUnitTest {
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
dependencies {
|
||||
// Data Injection
|
||||
classpath("com.google.dagger:hilt-android-gradle-plugin:2.44.2")
|
||||
|
||||
// Safe Navigation
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3")
|
||||
|
||||
// OSS licenses
|
||||
classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +20,10 @@ plugins {
|
||||
id("com.android.library") version "7.4.0" apply false
|
||||
|
||||
// add kotlin compatibility :>
|
||||
id("org.jetbrains.kotlin.android") version "1.8.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
||||
|
||||
// Hilt
|
||||
id("com.google.dagger.hilt.android") version "2.44" apply false
|
||||
}
|
||||
|
||||
// Cleanup the build directories
|
||||
|
Loading…
x
Reference in New Issue
Block a user