mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-22 10:52:13 +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
|
namespace = appID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Dzeio Charts
|
// Dzeio Charts
|
||||||
implementation("com.dzeio:charts:fe20f90654")
|
implementation("com.dzeio:charts:fe20f90654")
|
||||||
@ -198,6 +202,7 @@ dependencies {
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||||
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
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
|
package com.dzeio.openhealth
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
|
|||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("com.dzeio.openhealth", appContext.packageName)
|
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 -->
|
<!-- Phone Sensors for Steps -->
|
||||||
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
<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
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:allowBackup="true"
|
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)
|
notifyItemInserted(len)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
val len = this.items.size
|
||||||
|
this.items.clear()
|
||||||
|
notifyItemRangeRemoved(0, len)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to inflate the Adapter Bindings
|
* Function to inflate the Adapter Bindings
|
||||||
*
|
*
|
||||||
|
@ -15,6 +15,9 @@ interface BaseDao<T> {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(obj: T): Long
|
suspend fun insert(obj: T): Long
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAll(vararg obj: T)
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
suspend fun update(vararg obj: T)
|
suspend fun update(vararg obj: T)
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ package com.dzeio.openhealth.core
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple Observable implementation
|
* Simple Observable implementation
|
||||||
@ -54,4 +56,19 @@ open class Observable<T>(baseValue: T) {
|
|||||||
}
|
}
|
||||||
return ld
|
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
|
package com.dzeio.openhealth.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
@ -25,8 +26,10 @@ import com.dzeio.openhealth.data.weight.WeightDao
|
|||||||
Step::class,
|
Step::class,
|
||||||
Food::class
|
Food::class
|
||||||
],
|
],
|
||||||
// TODO: go back to version 1 when the app is published
|
version = 2,
|
||||||
version = 1,
|
autoMigrations = [
|
||||||
|
AutoMigration(1, 2)
|
||||||
|
],
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
@ -69,7 +69,8 @@ data class Food(
|
|||||||
if (
|
if (
|
||||||
food.nutriments == null ||
|
food.nutriments == null ||
|
||||||
food.name == 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,12 +94,12 @@ data class Food(
|
|||||||
// do some slight edit on the serving to remove strange entries like `100 g`
|
// do some slight edit on the serving to remove strange entries like `100 g`
|
||||||
serving = (food.servingSize ?: food.quantity ?: "unknown").replace(Regex(" +"), ""),
|
serving = (food.servingSize ?: food.quantity ?: "unknown").replace(Regex(" +"), ""),
|
||||||
quantity = eaten,
|
quantity = eaten,
|
||||||
proteins = food.nutriments.proteins,
|
proteins = food.nutriments!!.proteins,
|
||||||
carbohydrates = food.nutriments.carbohydrates,
|
carbohydrates = food.nutriments!!.carbohydrates,
|
||||||
fat = food.nutriments.fat,
|
fat = food.nutriments!!.fat,
|
||||||
// handle case where the energy is not given in kcal but only in kj
|
// handle case where the energy is not given in kcal but only in kj
|
||||||
energy = food.nutriments.energy
|
energy = food.nutriments!!.energy
|
||||||
?: (food.nutriments.energyKJ * 0.2390057361).toFloat(),
|
?: (food.nutriments!!.energyKJ * 0.2390057361).toFloat(),
|
||||||
image = food.image
|
image = food.image
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -18,5 +18,4 @@ interface FoodDao : BaseDao<Food> {
|
|||||||
|
|
||||||
@Query("Select * FROM Food ORDER BY timestamp DESC LIMIT 1")
|
@Query("Select * FROM Food ORDER BY timestamp DESC LIMIT 1")
|
||||||
fun last(): Flow<Food?>
|
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.data.openfoodfact.OpenFoodFactService
|
||||||
import com.dzeio.openhealth.utils.NetworkResult
|
import com.dzeio.openhealth.utils.NetworkResult
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class FoodRepository @Inject constructor(
|
class FoodRepository @Inject constructor(
|
||||||
private val dao: FoodDao,
|
private val dao: FoodDao,
|
||||||
|
@ -43,7 +43,7 @@ data class OFFProduct(
|
|||||||
* the product nutriments
|
* the product nutriments
|
||||||
*/
|
*/
|
||||||
@SerializedName("nutriments")
|
@SerializedName("nutriments")
|
||||||
var nutriments: OFFNutriments,
|
var nutriments: OFFNutriments?,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the product image
|
* the product image
|
||||||
|
@ -34,15 +34,20 @@ interface OpenFoodFactService {
|
|||||||
/**
|
/**
|
||||||
* Search a product by it's name
|
* Search a product by it's name
|
||||||
*/
|
*/
|
||||||
@Headers("User-Agent: OpenHealth - Android - Version ${BuildConfig.VERSION_NAME} - https://github.com/dzeiocom/OpenHealth")
|
@Headers(
|
||||||
@GET("/cgi/search.pl?json=true&fields=_id,nutriments,product_name,serving_quantity,serving_size,quantity,product_quantity,image_url&action=process")
|
"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>
|
suspend fun searchProducts(@Query("search_terms2") name: String): Response<OFFResult>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search a product by it's barcode
|
* 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")
|
@GET("/api/v2/search?fields=_id,nutriments,product_name,serving_quantity")
|
||||||
suspend fun findByCode(@Query("code") code: String): Response<OFFResult>
|
suspend fun findByCode(@Query("code") code: String): Response<OFFResult>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package com.dzeio.openhealth.data.step
|
package com.dzeio.openhealth.data.step
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class StepRepository @Inject constructor(
|
class StepRepository @Inject constructor(
|
||||||
@ -46,5 +46,4 @@ class StepRepository @Inject constructor(
|
|||||||
fun currentStep() = lastStep().filter {
|
fun currentStep() = lastStep().filter {
|
||||||
return@filter it != null && it.isCurrent()
|
return@filter it != null && it.isCurrent()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import android.hardware.SensorEventListener
|
|||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.dzeio.openhealth.Application
|
import com.dzeio.openhealth.Application
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@ -18,7 +19,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
* TODO: rewrite to use the new libs
|
* TODO: rewrite to use the new libs
|
||||||
*/
|
*/
|
||||||
class StepSource(
|
class StepSource(
|
||||||
private val context: Context,
|
context: Context,
|
||||||
private val callback: ((Float) -> Unit)? = null
|
private val callback: ((Float) -> Unit)? = null
|
||||||
) : SensorEventListener {
|
) : SensorEventListener {
|
||||||
|
|
||||||
@ -33,27 +34,31 @@ class StepSource(
|
|||||||
return prefs.getLong("steps_time_since_last_record", Long.MAX_VALUE)
|
return prefs.getLong("steps_time_since_last_record", Long.MAX_VALUE)
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
val editor = prefs.edit()
|
prefs.edit {
|
||||||
editor.putLong("steps_time_since_last_record", value)
|
putLong("steps_time_since_last_record", value)
|
||||||
editor.commit()
|
}
|
||||||
}
|
}
|
||||||
private var stepsAsOfLastRecord: Float
|
private var stepsAsOfLastRecord: Float
|
||||||
get() {
|
get() {
|
||||||
return prefs.getFloat("steps_as_of_last_record", 0f)
|
return prefs.getFloat("steps_as_of_last_record", 0f)
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
val editor = prefs.edit()
|
prefs.edit {
|
||||||
editor.putFloat("steps_as_of_last_record", value)
|
putFloat("steps_as_of_last_record", value)
|
||||||
editor.commit()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG, "Setting up")
|
Log.d(TAG, "Setting up")
|
||||||
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
val stepCountSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
|
val stepCountSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
|
||||||
stepCountSensor.let {
|
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")
|
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
|
// don't send changes since it wasn't made when the app was running
|
||||||
if (timeSinceLastBoot < timeSinceLastRecord) {
|
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
|
timeSinceLastRecord = timeSinceLastBoot
|
||||||
return@let
|
return@let
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package com.dzeio.openhealth.data.water
|
package com.dzeio.openhealth.data.water
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class WaterRepository @Inject constructor(
|
class WaterRepository @Inject constructor(
|
||||||
@ -23,4 +21,4 @@ class WaterRepository @Inject constructor(
|
|||||||
fun todayWater() = lastWater().filter {
|
fun todayWater() = lastWater().filter {
|
||||||
return@filter it != null && it.isToday()
|
return@filter it != null && it.isToday()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,65 @@ data class Weight(
|
|||||||
*
|
*
|
||||||
* note: Unused currently but kept for future usage
|
* 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")
|
@Query("DELETE FROM Weight where source = :source")
|
||||||
suspend fun deleteFromSource(source: String)
|
suspend fun deleteFromSource(source: String)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ class WeightRepository @Inject constructor(
|
|||||||
fun getWeight(id: Long) = weightDao.getOne(id)
|
fun getWeight(id: Long) = weightDao.getOne(id)
|
||||||
|
|
||||||
suspend fun addWeight(weight: Weight) = weightDao.insert(weight)
|
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)
|
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.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.dzeio.openhealth.utils.Bluetooth
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@ -29,4 +30,8 @@ class SystemModule {
|
|||||||
fun provideConfig(sharedPreferences: SharedPreferences): Configuration {
|
fun provideConfig(sharedPreferences: SharedPreferences): Configuration {
|
||||||
return Configuration(sharedPreferences)
|
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.data.step.StepSource
|
||||||
import com.dzeio.openhealth.interfaces.NotificationChannels
|
import com.dzeio.openhealth.interfaces.NotificationChannels
|
||||||
import com.dzeio.openhealth.interfaces.NotificationIds
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@ -101,7 +103,7 @@ class OpenHealthService : Service() {
|
|||||||
if (step != null) {
|
if (step != null) {
|
||||||
step.value += stepsBuffer
|
step.value += stepsBuffer
|
||||||
stepRepository.updateStep(step)
|
stepRepository.updateStep(step)
|
||||||
// create a new steps object and send it
|
// create a new steps object and send it
|
||||||
} else {
|
} else {
|
||||||
stepRepository.addStep(Step(value = stepsBuffer))
|
stepRepository.addStep(Step(value = stepsBuffer))
|
||||||
}
|
}
|
||||||
@ -119,7 +121,7 @@ class OpenHealthService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
stopForeground(true)
|
stopForegroundPoly(NotificationBehavior.REMOVE)
|
||||||
|
|
||||||
// Tell the user we stopped.
|
// Tell the user we stopped.
|
||||||
Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show()
|
||||||
|
@ -116,7 +116,6 @@ class BrowseFragment :
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStepsText(numberOfSteps: Int?, goal: Int?) {
|
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.data.weight.WeightRepository
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BrowseViewModel @Inject internal constructor(
|
class BrowseViewModel @Inject internal constructor(
|
||||||
stepRepository: StepRepository,
|
stepRepository: StepRepository,
|
||||||
weightRepository: WeightRepository,
|
weightRepository: WeightRepository,
|
||||||
private val config: Configuration
|
config: Configuration
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val _steps = MutableLiveData(0)
|
private val _steps = MutableLiveData(0)
|
||||||
@ -41,6 +41,5 @@ class BrowseViewModel @Inject internal constructor(
|
|||||||
_weight.postValue(it.weight)
|
_weight.postValue(it.weight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ class FoodDialog :
|
|||||||
|
|
||||||
private var quantity: Float? = null
|
private var quantity: Float? = null
|
||||||
|
|
||||||
|
|
||||||
override val bindingInflater: (LayoutInflater) -> DialogFoodProductBinding =
|
override val bindingInflater: (LayoutInflater) -> DialogFoodProductBinding =
|
||||||
DialogFoodProductBinding::inflate
|
DialogFoodProductBinding::inflate
|
||||||
|
|
||||||
@ -50,7 +49,6 @@ class FoodDialog :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreated() {
|
override fun onCreated() {
|
||||||
super.onCreated()
|
super.onCreated()
|
||||||
Log.d("FoodDialog", args.id.toString())
|
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.Food
|
||||||
import com.dzeio.openhealth.data.food.FoodRepository
|
import com.dzeio.openhealth.data.food.FoodRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FoodDialogViewModel @Inject internal constructor(
|
class FoodDialogViewModel @Inject internal constructor(
|
||||||
@ -41,7 +41,7 @@ class FoodDialogViewModel @Inject internal constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val transformer = (quantity ?: it.quantity) / it.quantity
|
val transformer = quantity / it.quantity
|
||||||
it.energy = it.energy * transformer
|
it.energy = it.energy * transformer
|
||||||
it.proteins = it.proteins * transformer
|
it.proteins = it.proteins * transformer
|
||||||
it.carbohydrates = it.carbohydrates * transformer
|
it.carbohydrates = it.carbohydrates * transformer
|
||||||
|
@ -64,7 +64,6 @@ class FoodHomeFragment :
|
|||||||
proteins += food.proteins / 100 * food.quantity
|
proteins += food.proteins / 100 * food.quantity
|
||||||
carbohydrates += food.carbohydrates / 100 * food.quantity
|
carbohydrates += food.carbohydrates / 100 * food.quantity
|
||||||
fat += food.fat / 100 * food.quantity
|
fat += food.fat / 100 * food.quantity
|
||||||
|
|
||||||
}
|
}
|
||||||
binding.energyTxt.text = "${energy.toInt()} / 2594kcal"
|
binding.energyTxt.text = "${energy.toInt()} / 2594kcal"
|
||||||
binding.energyBar.progress = (100 * energy / 2594).toInt()
|
binding.energyBar.progress = (100 * energy / 2594).toInt()
|
||||||
@ -87,7 +86,9 @@ class FoodHomeFragment :
|
|||||||
viewModel.date.observe(viewLifecycleOwner) {
|
viewModel.date.observe(viewLifecycleOwner) {
|
||||||
val date = Calendar.getInstance()
|
val date = Calendar.getInstance()
|
||||||
date.timeInMillis = it
|
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.core.BaseViewModel
|
||||||
import com.dzeio.openhealth.data.food.Food
|
import com.dzeio.openhealth.data.food.Food
|
||||||
import com.dzeio.openhealth.data.food.FoodRepository
|
import com.dzeio.openhealth.data.food.FoodRepository
|
||||||
import com.dzeio.openhealth.data.openfoodfact.OpenFoodFactService
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FoodHomeViewModel @Inject internal constructor(
|
class FoodHomeViewModel @Inject internal constructor(
|
||||||
private val foodRepository: FoodRepository,
|
private val foodRepository: FoodRepository
|
||||||
private val foodFactService: OpenFoodFactService
|
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
val items: MutableLiveData<List<Food>> = MutableLiveData()
|
val items: MutableLiveData<List<Food>> = MutableLiveData()
|
||||||
private val list: MutableLiveData<List<Food>> = MutableLiveData(arrayListOf())
|
private val list: MutableLiveData<List<Food>> = MutableLiveData(arrayListOf())
|
||||||
@ -62,8 +60,10 @@ class FoodHomeViewModel @Inject internal constructor(
|
|||||||
day.add(Calendar.DAY_OF_YEAR, 1)
|
day.add(Calendar.DAY_OF_YEAR, 1)
|
||||||
val tomorrow = day.timeInMillis
|
val tomorrow = day.timeInMillis
|
||||||
|
|
||||||
items.postValue((foods ?: list.value!!).filter { food ->
|
items.postValue(
|
||||||
food.timestamp in todayInMillis until tomorrow
|
(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.data.food.FoodRepository
|
||||||
import com.dzeio.openhealth.utils.NetworkResult
|
import com.dzeio.openhealth.utils.NetworkResult
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchFoodDialogViewModel @Inject internal constructor(
|
class SearchFoodDialogViewModel @Inject internal constructor(
|
||||||
|
@ -7,12 +7,14 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.dzeio.charts.Entry
|
import com.dzeio.charts.Entry
|
||||||
import com.dzeio.charts.axis.Line
|
import com.dzeio.charts.axis.Line
|
||||||
import com.dzeio.charts.series.LineSerie
|
import com.dzeio.charts.series.LineSerie
|
||||||
|
import com.dzeio.openhealth.BuildConfig
|
||||||
import com.dzeio.openhealth.core.BaseFragment
|
import com.dzeio.openhealth.core.BaseFragment
|
||||||
import com.dzeio.openhealth.data.water.Water
|
import com.dzeio.openhealth.data.water.Water
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
@ -98,6 +100,17 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
|||||||
series = arrayListOf(serie)
|
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
|
// Update the water intake Graph when the water intake changes
|
||||||
viewModel.water.observe(viewLifecycleOwner) {
|
viewModel.water.observe(viewLifecycleOwner) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
@ -218,7 +231,7 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
|||||||
animator.duration = 300 // ms
|
animator.duration = 300 // ms
|
||||||
animator.addUpdateListener {
|
animator.addUpdateListener {
|
||||||
this.oldValue = (it.animatedValue as Int).toFloat()
|
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 graph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
val canvas = Canvas(graph)
|
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.units.Units
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject internal constructor(
|
class HomeViewModel @Inject internal constructor(
|
||||||
|
@ -40,7 +40,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||||||
// Force only numbers on Goal
|
// Force only numbers on Goal
|
||||||
val weightGoal = findPreference<EditTextPreference>("tmp_goal_weight")
|
val weightGoal = findPreference<EditTextPreference>("tmp_goal_weight")
|
||||||
weightGoal?.apply {
|
weightGoal?.apply {
|
||||||
|
|
||||||
setOnBindEditTextListener {
|
setOnBindEditTextListener {
|
||||||
it.setSelectAllOnFocus(true)
|
it.setSelectAllOnFocus(true)
|
||||||
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||||
@ -60,7 +59,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||||||
return@setOnPreferenceClickListener true
|
return@setOnPreferenceClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
val unit = config.getString(Settings.MASS_UNIT).value
|
val unit = config.getString(Settings.MASS_UNIT).value
|
||||||
var modifier = Units.Mass.KILOGRAM.modifier
|
var modifier = Units.Mass.KILOGRAM.modifier
|
||||||
@ -91,10 +89,5 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||||||
|
|
||||||
return@setOnPreferenceChangeListener true
|
return@setOnPreferenceChangeListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
val stepsGoalPreference = findPreference<EditTextPreference>(Settings.STEPS_GOAL)
|
|
||||||
stepsGoalPreference.apply {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,8 +129,12 @@ class StepsHomeFragment :
|
|||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
|
|
||||||
val filtered = if (!isDay) list else list.filter {
|
val filtered = if (!isDay) {
|
||||||
it.getDay() == args.day
|
list
|
||||||
|
} else {
|
||||||
|
list.filter {
|
||||||
|
it.getDay() == args.day
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isDay) {
|
if (isDay) {
|
||||||
adapter.set(filtered)
|
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.data.step.StepRepository
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class StepsHomeViewModel@Inject internal constructor(
|
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 ->
|
binding.editTextNumber.doOnTextChanged { text, start, before, count ->
|
||||||
val value = text.toString()
|
val value = text.toString()
|
||||||
newValue = if (value == "") 0
|
newValue = if (value == "") {
|
||||||
else text.toString().toInt()
|
0
|
||||||
|
} else {
|
||||||
|
text.toString().toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.date.setOnClickListener {
|
binding.date.setOnClickListener {
|
||||||
@ -100,6 +103,7 @@ class EditWaterDialog :
|
|||||||
findNavController().popBackStack()
|
findNavController().popBackStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.menu_fullscreen_dialog_save -> {
|
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.Water
|
||||||
import com.dzeio.openhealth.data.water.WaterRepository
|
import com.dzeio.openhealth.data.water.WaterRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class EditWaterViewModel @Inject internal constructor(
|
class EditWaterViewModel @Inject internal constructor(
|
||||||
@ -36,4 +36,4 @@ class EditWaterViewModel @Inject internal constructor(
|
|||||||
waterRepository.addWater(water)
|
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.Water
|
||||||
import com.dzeio.openhealth.data.water.WaterRepository
|
import com.dzeio.openhealth.data.water.WaterRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class WaterHomeViewModel@Inject internal constructor(
|
class WaterHomeViewModel@Inject internal constructor(
|
||||||
@ -23,4 +23,4 @@ class WaterHomeViewModel@Inject internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package com.dzeio.openhealth.ui.water
|
package com.dzeio.openhealth.ui.water
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.marginBottom
|
|
||||||
import com.dzeio.openhealth.R
|
import com.dzeio.openhealth.R
|
||||||
import com.dzeio.openhealth.core.BaseDialog
|
import com.dzeio.openhealth.core.BaseDialog
|
||||||
import com.dzeio.openhealth.databinding.DialogWaterSizeSelectorBinding
|
import com.dzeio.openhealth.databinding.DialogWaterSizeSelectorBinding
|
||||||
|
@ -23,7 +23,9 @@ import java.util.Date
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class EditWeightDialog :
|
class EditWeightDialog :
|
||||||
BaseFullscreenDialog<EditWeightDialogViewModel, DialogEditWeightBinding>(EditWeightDialogViewModel::class.java) {
|
BaseFullscreenDialog<EditWeightDialogViewModel, DialogEditWeightBinding>(
|
||||||
|
EditWeightDialogViewModel::class.java
|
||||||
|
) {
|
||||||
|
|
||||||
override val bindingInflater: (LayoutInflater) -> DialogEditWeightBinding =
|
override val bindingInflater: (LayoutInflater) -> DialogEditWeightBinding =
|
||||||
DialogEditWeightBinding::inflate
|
DialogEditWeightBinding::inflate
|
||||||
@ -97,10 +99,7 @@ class EditWeightDialog :
|
|||||||
} else {
|
} else {
|
||||||
TODO("VERSION.SDK_INT < N")
|
TODO("VERSION.SDK_INT < N")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
@ -140,6 +139,5 @@ class EditWeightDialog :
|
|||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
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.units.Units
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class EditWeightDialogViewModel @Inject internal constructor(
|
class EditWeightDialogViewModel @Inject internal constructor(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.dzeio.openhealth.ui.weight
|
package com.dzeio.openhealth.ui.weight
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -9,6 +10,8 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
@ -16,11 +19,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import com.dzeio.charts.Entry
|
import com.dzeio.charts.Entry
|
||||||
import com.dzeio.charts.axis.Line
|
import com.dzeio.charts.axis.Line
|
||||||
import com.dzeio.charts.series.LineSerie
|
import com.dzeio.charts.series.LineSerie
|
||||||
|
import com.dzeio.openhealth.BuildConfig
|
||||||
import com.dzeio.openhealth.R
|
import com.dzeio.openhealth.R
|
||||||
import com.dzeio.openhealth.adapters.WeightAdapter
|
import com.dzeio.openhealth.adapters.WeightAdapter
|
||||||
import com.dzeio.openhealth.core.BaseFragment
|
import com.dzeio.openhealth.core.BaseFragment
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
import com.dzeio.openhealth.databinding.FragmentListWeightBinding
|
import com.dzeio.openhealth.databinding.FragmentListWeightBinding
|
||||||
|
import com.dzeio.openhealth.utils.PermissionsManager
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -34,6 +39,40 @@ class ListWeightFragment :
|
|||||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) ->
|
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) ->
|
||||||
FragmentListWeightBinding = FragmentListWeightBinding::inflate
|
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 {
|
val settings: SharedPreferences by lazy {
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
}
|
}
|
||||||
@ -42,26 +81,7 @@ class ListWeightFragment :
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// Menu
|
// Menu
|
||||||
requireActivity().addMenuProvider(object : MenuProvider {
|
requireActivity().addMenuProvider(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (viewModel.goalWeight.value != null) {
|
if (viewModel.goalWeight.value != null) {
|
||||||
binding.goalButton.setText(R.string.edit_goal)
|
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 {
|
val adapter = WeightAdapter().apply {
|
||||||
onItemClick = {
|
onItemClick = {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
@ -152,15 +190,21 @@ class ListWeightFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Debug button
|
// Debug button
|
||||||
// if (binding.debugRandomValues != null) {
|
if (BuildConfig.DEBUG) {
|
||||||
// binding.debugRandomValues.setOnClickListener {
|
binding.debugRandomValues.visibility = View.VISIBLE
|
||||||
// viewModel.generateRandomValues()
|
binding.debugRandomValues.setOnClickListener {
|
||||||
// }
|
viewModel.generateRandomValues()
|
||||||
// binding.debugRandomValues.setOnLongClickListener {
|
}
|
||||||
// viewModel.delete(viewModel.weights.value!!)
|
binding.debugRandomValues.setOnLongClickListener {
|
||||||
// return@setOnLongClickListener true
|
viewModel.delete(viewModel.weights.value!!)
|
||||||
// }
|
return@setOnLongClickListener true
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
requireActivity().removeMenuProvider(menuProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGraph(list: List<Weight>) {
|
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.units.Units
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ListWeightViewModel @Inject internal constructor(
|
class ListWeightViewModel @Inject internal constructor(
|
||||||
private val weightRepository: WeightRepository,
|
private val weightRepository: WeightRepository,
|
||||||
private val settings: Configuration
|
settings: Configuration
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val _massUnit = MutableLiveData(Units.Mass.KILOGRAM)
|
private val _massUnit = MutableLiveData(Units.Mass.KILOGRAM)
|
||||||
@ -60,7 +60,7 @@ class ListWeightViewModel @Inject internal constructor(
|
|||||||
|
|
||||||
fun delete(list: List<Weight>) {
|
fun delete(list: List<Weight>) {
|
||||||
viewModelScope.launch {
|
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.maxValue = 9
|
||||||
binding.gram.minValue = 0
|
binding.gram.minValue = 0
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setValue(value: Float) {
|
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.units.Units
|
||||||
import com.dzeio.openhealth.utils.Configuration
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class WeightDialogViewModel @Inject internal constructor(
|
class WeightDialogViewModel @Inject internal constructor(
|
||||||
private val weightRepository: WeightRepository,
|
private val weightRepository: WeightRepository,
|
||||||
private val settings: Configuration
|
settings: Configuration
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val _goalWeight = settings.getFloat(Settings.WEIGHT_GOAL)
|
private val _goalWeight = settings.getFloat(Settings.WEIGHT_GOAL)
|
||||||
@ -40,7 +40,6 @@ class WeightDialogViewModel @Inject internal constructor(
|
|||||||
weightRepository.lastWeight().collectLatest {
|
weightRepository.lastWeight().collectLatest {
|
||||||
_weight.postValue(it)
|
_weight.postValue(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,6 @@ object Units {
|
|||||||
it.id == value
|
it.id == value
|
||||||
} ?: KILOGRAM
|
} ?: KILOGRAM
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun format(value: Float): Float {
|
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
|
package com.dzeio.openhealth.utils
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.dzeio.openhealth.Application
|
import com.dzeio.openhealth.Application
|
||||||
import com.dzeio.openhealth.core.Observable
|
import com.dzeio.openhealth.core.Observable
|
||||||
@ -27,66 +26,66 @@ class Configuration(
|
|||||||
|
|
||||||
fun getString(key: String): StringField {
|
fun getString(key: String): StringField {
|
||||||
if (cache[key] == null) {
|
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)
|
cache[key] = StringField(key)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$key in cache")
|
// Log.d(TAG, "$key in cache")
|
||||||
}
|
}
|
||||||
return cache[key] as StringField
|
return cache[key] as StringField
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLong(key: String): LongField {
|
fun getLong(key: String): LongField {
|
||||||
if (cache[key] == null) {
|
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)
|
cache[key] = LongField(key)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$key in cache")
|
// Log.d(TAG, "$key in cache")
|
||||||
}
|
}
|
||||||
return cache[key] as LongField
|
return cache[key] as LongField
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBoolean(key: String): BooleanField {
|
fun getBoolean(key: String): BooleanField {
|
||||||
if (cache[key] == null) {
|
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)
|
cache[key] = BooleanField(key)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$key in cache")
|
// Log.d(TAG, "$key in cache")
|
||||||
}
|
}
|
||||||
return cache[key] as BooleanField
|
return cache[key] as BooleanField
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getInt(key: String): IntField {
|
fun getInt(key: String): IntField {
|
||||||
if (cache[key] == null) {
|
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)
|
cache[key] = IntField(key)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$key in cache")
|
// Log.d(TAG, "$key in cache")
|
||||||
}
|
}
|
||||||
return cache[key] as IntField
|
return cache[key] as IntField
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFloat(key: String): FloatField {
|
fun getFloat(key: String): FloatField {
|
||||||
if (cache[key] == null) {
|
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)
|
cache[key] = FloatField(key)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$key in cache")
|
// Log.d(TAG, "$key in cache")
|
||||||
}
|
}
|
||||||
return cache[key] as FloatField
|
return cache[key] as FloatField
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStringSet(key: String): StringSetField {
|
fun getStringSet(key: String): StringSetField {
|
||||||
if (cache[key] == null) {
|
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)
|
cache[key] = StringSetField(key)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "$key in cache")
|
// Log.d(TAG, "$key in cache")
|
||||||
}
|
}
|
||||||
return cache[key] as StringSetField
|
return cache[key] as StringSetField
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(u: SharedPreferences, key: String) {
|
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]?.needUpdate = true
|
||||||
cache[key]?.notifyObservers()
|
cache[key]?.notifyObservers()
|
||||||
}
|
}
|
||||||
|
@ -38,5 +38,4 @@ object DrawUtils {
|
|||||||
val it = (if (isWidth) this.width else this.height) * multiplier
|
val it = (if (isWidth) this.width else this.height) * multiplier
|
||||||
return it * value / 100
|
return it * value / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,10 @@ import androidx.preference.PreferenceManager
|
|||||||
import com.dzeio.openhealth.Settings
|
import com.dzeio.openhealth.Settings
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utils object for [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 {
|
object LocaleUtils {
|
||||||
fun onAttach(context: Context): Context {
|
fun onAttach(context: Context): Context {
|
||||||
@ -68,8 +67,10 @@ object LocaleUtils {
|
|||||||
Locale.setDefault(locale)
|
Locale.setDefault(locale)
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
val configuration: Configuration = resources.configuration
|
val configuration: Configuration = resources.configuration
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
configuration.locale = locale
|
configuration.locale = locale
|
||||||
configuration.setLayoutDirection(locale)
|
configuration.setLayoutDirection(locale)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
resources.updateConfiguration(configuration, resources.displayMetrics)
|
resources.updateConfiguration(configuration, resources.displayMetrics)
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
@ -36,4 +36,3 @@ object PermissionsManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Log
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
|
|
||||||
@ -48,7 +47,6 @@ class IntEditTextPreference : EditTextPreference, EditTextPreference.OnBindEditT
|
|||||||
* @param text The text to save
|
* @param text The text to save
|
||||||
*/
|
*/
|
||||||
override fun setText(text: String?) {
|
override fun setText(text: String?) {
|
||||||
|
|
||||||
val wasBlocking = shouldDisableDependents()
|
val wasBlocking = shouldDisableDependents()
|
||||||
val pouet = Integer.parseInt(text.toString())
|
val pouet = Integer.parseInt(text.toString())
|
||||||
this.txt = text
|
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"
|
android:orientation="vertical"
|
||||||
tools:context=".ui.home.HomeFragment">
|
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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -306,7 +313,7 @@
|
|||||||
<com.dzeio.charts.ChartView
|
<com.dzeio.charts.ChartView
|
||||||
android:id="@+id/weight_graph"
|
android:id="@+id/weight_graph"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="200dp"
|
android:layout_height="150dp"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
android:minHeight="200dp" />
|
android:minHeight="200dp" />
|
||||||
|
|
||||||
|
@ -6,6 +6,62 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_height="wrap_content">
|
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
|
<com.google.android.material.card.MaterialCardView
|
||||||
style="?attr/materialCardViewFilledStyle"
|
style="?attr/materialCardViewFilledStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -74,6 +130,14 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:id="@+id/list"
|
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
|
<action
|
||||||
android:id="@+id/action_nav_home_to_nav_weight_dialog"
|
android:id="@+id/action_nav_home_to_nav_weight_dialog"
|
||||||
app:destination="@id/nav_weight_dialog" />
|
app:destination="@id/nav_weight_dialog" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_nav_home_to_testsFragment"
|
||||||
|
app:destination="@id/testsFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
@ -43,6 +46,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_nav_list_weight_to_nav_weight_dialog"
|
android:id="@+id/action_nav_list_weight_to_nav_weight_dialog"
|
||||||
app:destination="@id/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>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
@ -196,4 +202,14 @@
|
|||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
/>
|
/>
|
||||||
</dialog>
|
</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>
|
</navigation>
|
||||||
|
@ -68,4 +68,5 @@
|
|||||||
<string name="food_description" translatable="false">%1$s (%2$.0f kcal)</string>
|
<string name="food_description" translatable="false">%1$s (%2$.0f kcal)</string>
|
||||||
<string name="steps_count">%1$d steps</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="connectivity_error">It seems that we can\'t communicate with OpenFoodFact, please retry later</string>
|
||||||
|
<string name="searching_scales">Searchin Scales</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package com.dzeio.openhealth
|
package com.dzeio.openhealth
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
@ -14,4 +13,4 @@ class ExampleUnitTest {
|
|||||||
fun addition_isCorrect() {
|
fun addition_isCorrect() {
|
||||||
assertEquals(4, 2 + 2)
|
assertEquals(4, 2 + 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
dependencies {
|
dependencies {
|
||||||
// Data Injection
|
|
||||||
classpath("com.google.dagger:hilt-android-gradle-plugin:2.44.2")
|
|
||||||
|
|
||||||
// Safe Navigation
|
// Safe Navigation
|
||||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3")
|
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3")
|
||||||
|
|
||||||
// OSS licenses
|
// OSS licenses
|
||||||
classpath("com.google.android.gms:oss-licenses-plugin:0.10.6")
|
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
|
id("com.android.library") version "7.4.0" apply false
|
||||||
|
|
||||||
// add kotlin compatibility :>
|
// 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
|
// Cleanup the build directories
|
||||||
|
Loading…
x
Reference in New Issue
Block a user