mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-23 03:12:14 +00:00
feat: Started changes to new Google extension
Signed-off-by: Avior <github@avior.me>
This commit is contained in:
parent
a9a488b122
commit
7b3f409733
@ -23,12 +23,12 @@
|
|||||||
|
|
||||||
## Privacy and Permissions
|
## Privacy and Permissions
|
||||||
|
|
||||||
No Ads are served through this app.
|
No Ads, no tracking.
|
||||||
|
|
||||||
Permissions requests are for specifics usage and are only requests the first time they are needed:
|
Permissions requests are for specifics usage and are only requests the first time they are needed:
|
||||||
|
|
||||||
| Permission | Why is it requested |
|
| Permission | Why is it requested |
|
||||||
| :--------------------: |:-----------------------------------------------------------------|
|
|:----------------------:|:-----------------------------------------------------------------|
|
||||||
| ACCESS_FINE_LOCATION | Google Fit Extension Requirement (maybe not, still have to test) |
|
| ACCESS_FINE_LOCATION | Google Fit Extension Requirement (maybe not, still have to test) |
|
||||||
| ACCESS_COARSE_LOCATION | Same as above |
|
| ACCESS_COARSE_LOCATION | Same as above |
|
||||||
| ACTIVITY_RECOGNITION | Device Steps Usage |
|
| ACTIVITY_RECOGNITION | Device Steps Usage |
|
||||||
|
@ -15,6 +15,14 @@ plugins {
|
|||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appID = "com.dzeio.openhealth"
|
||||||
|
|
||||||
|
// Languages
|
||||||
|
val locales = listOf("en", "fr")
|
||||||
|
|
||||||
|
val sdkMin = 21
|
||||||
|
val sdkTarget = 33
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@ -37,17 +45,17 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 33
|
compileSdk = sdkTarget
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// App ID
|
// App ID
|
||||||
applicationId = "com.dzeio.openhealth"
|
applicationId = appID
|
||||||
|
|
||||||
// Android 5 Lollipop
|
// Android 5 Lollipop
|
||||||
minSdk = 21
|
minSdk = sdkMin
|
||||||
|
|
||||||
// Android 12
|
// Android 12
|
||||||
targetSdk = 33
|
targetSdk = sdkTarget
|
||||||
|
|
||||||
// Semantic Versioning
|
// Semantic Versioning
|
||||||
versionName = "1.0.0"
|
versionName = "1.0.0"
|
||||||
@ -55,8 +63,11 @@ android {
|
|||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// Languages
|
kapt {
|
||||||
val locales = listOf("en", "fr")
|
arguments {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String[]",
|
"String[]",
|
||||||
@ -104,7 +115,7 @@ android {
|
|||||||
viewBinding = true
|
viewBinding = true
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
}
|
}
|
||||||
namespace = "com.dzeio.openhealth"
|
namespace = appID
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -115,10 +126,10 @@ dependencies {
|
|||||||
implementation("com.dzeio:crashhandler:1.0.1")
|
implementation("com.dzeio:crashhandler:1.0.1")
|
||||||
|
|
||||||
// Core dependencies
|
// Core dependencies
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.0-beta01")
|
implementation("androidx.appcompat:appcompat:1.7.0-alpha01")
|
||||||
implementation("javax.inject:javax.inject:1")
|
implementation("javax.inject:javax.inject:1")
|
||||||
implementation("com.google.android.material:material:1.7.0-beta01")
|
implementation("com.google.android.material:material:1.8.0-alpha02")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
||||||
@ -135,8 +146,8 @@ dependencies {
|
|||||||
implementation("androidx.datastore:datastore:1.0.0")
|
implementation("androidx.datastore:datastore:1.0.0")
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
|
implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
|
||||||
|
|
||||||
// Paging
|
// Paging
|
||||||
implementation("androidx.paging:paging-runtime:3.1.1")
|
implementation("androidx.paging:paging-runtime:3.1.1")
|
||||||
@ -148,8 +159,8 @@ dependencies {
|
|||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||||
|
|
||||||
// Graph
|
// Graph
|
||||||
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
|
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
|
||||||
@ -163,7 +174,8 @@ dependencies {
|
|||||||
|
|
||||||
// Google Fit
|
// Google Fit
|
||||||
implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
implementation("com.google.android.gms:play-services-fitness:21.1.0")
|
||||||
implementation("com.google.android.gms:play-services-auth:20.2.0")
|
implementation("com.google.android.gms:play-services-auth:20.3.0")
|
||||||
|
implementation("androidx.health.connect:connect-client:1.0.0-alpha07")
|
||||||
|
|
||||||
// Samsung Health
|
// Samsung Health
|
||||||
implementation(files("libs/samsung-health-data-1.5.0.aar"))
|
implementation(files("libs/samsung-health-data-1.5.0.aar"))
|
||||||
|
158
app/schemas/com.dzeio.openhealth.data.AppDatabase/1.json
Normal file
158
app/schemas/com.dzeio.openhealth.data.AppDatabase/1.json
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "2acd5897bbf15393886259605a1df934",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Weight",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `weight` REAL NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "weight",
|
||||||
|
"columnName": "weight",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Weight_timestamp",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Weight_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Water",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Water_timestamp",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Water_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Step",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `value` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `source` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Step_timestamp",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"timestamp"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Step_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2acd5897bbf15393886259605a1df934')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
@ -16,6 +16,7 @@
|
|||||||
<queries>
|
<queries>
|
||||||
<package android:name="com.sec.android.app.shealth" />
|
<package android:name="com.sec.android.app.shealth" />
|
||||||
</queries>
|
</queries>
|
||||||
|
<uses-sdk tools:overrideLibrary="androidx.health.connect.client" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
@ -59,6 +60,21 @@
|
|||||||
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
|
||||||
android:theme="@style/Theme.OpenHealth" />
|
android:theme="@style/Theme.OpenHealth" />
|
||||||
|
|
||||||
|
<!-- Activity to show rationale of Health Connect permissions -->
|
||||||
|
<activity
|
||||||
|
android:name=".ui.PrivacyPolicyActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- List of health data permissions -->
|
||||||
|
<meta-data
|
||||||
|
android:name="health_permissions"
|
||||||
|
android:resource="@array/health_permissions" />
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.OpenHealthService"
|
android:name=".services.OpenHealthService"
|
||||||
@ -73,6 +89,13 @@
|
|||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<package android:name="com.google.android.apps.healthdata" />
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package com.dzeio.openhealth
|
package com.dzeio.openhealth
|
||||||
|
|
||||||
|
import com.dzeio.openhealth.extensions.Extension
|
||||||
|
|
||||||
object Settings {
|
object Settings {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,4 +29,8 @@ object Settings {
|
|||||||
*/
|
*/
|
||||||
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
|
const val MASS_UNIT = "com.dzeio.open-health.unit.mass"
|
||||||
|
|
||||||
|
fun extensionEnabled(extension: Extension): String {
|
||||||
|
return "com.dzeio.open-health.extension.${extension.id}.enabled"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,17 @@ package com.dzeio.openhealth.adapters
|
|||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import com.dzeio.openhealth.Settings
|
||||||
import com.dzeio.openhealth.core.BaseAdapter
|
import com.dzeio.openhealth.core.BaseAdapter
|
||||||
import com.dzeio.openhealth.core.BaseViewHolder
|
import com.dzeio.openhealth.core.BaseViewHolder
|
||||||
import com.dzeio.openhealth.databinding.LayoutExtensionItemBinding
|
import com.dzeio.openhealth.databinding.LayoutExtensionItemBinding
|
||||||
import com.dzeio.openhealth.extensions.Extension
|
import com.dzeio.openhealth.extensions.Extension
|
||||||
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
|
|
||||||
|
|
||||||
class ExtensionAdapter : BaseAdapter<Extension, LayoutExtensionItemBinding>() {
|
class ExtensionAdapter(
|
||||||
|
private val config: Configuration
|
||||||
|
) : BaseAdapter<Extension, LayoutExtensionItemBinding>() {
|
||||||
|
|
||||||
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) ->
|
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) ->
|
||||||
LayoutExtensionItemBinding = LayoutExtensionItemBinding::inflate
|
LayoutExtensionItemBinding = LayoutExtensionItemBinding::inflate
|
||||||
@ -20,10 +24,15 @@ class ExtensionAdapter : BaseAdapter<Extension, LayoutExtensionItemBinding>() {
|
|||||||
item: Extension,
|
item: Extension,
|
||||||
position: Int
|
position: Int
|
||||||
) {
|
) {
|
||||||
|
val isEnabled = config.getBoolean(Settings.extensionEnabled(item)).value ?: false
|
||||||
holder.binding.name.text = item.name
|
holder.binding.name.text = item.name
|
||||||
holder.binding.status.text = item.getStatus()
|
holder.binding.card.isClickable = item.isAvailable()
|
||||||
|
holder.binding.card.isEnabled = item.isAvailable()
|
||||||
|
holder.binding.status.text = "enabled = $isEnabled"
|
||||||
|
if (item.isAvailable()) {
|
||||||
holder.binding.card.setOnClickListener {
|
holder.binding.card.setOnClickListener {
|
||||||
onItemClick?.invoke(item)
|
onItemClick?.invoke(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.dzeio.openhealth.core
|
package com.dzeio.openhealth.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -14,6 +14,14 @@ open class Observable<T>(baseValue: T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addOneTimeObserver(fn: (T) -> Unit) {
|
||||||
|
val index = functionObservers.size
|
||||||
|
functionObservers.add {
|
||||||
|
fn(it)
|
||||||
|
functionObservers.removeAt(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeObserver(fn: (T) -> Unit) {
|
fun removeObserver(fn: (T) -> Unit) {
|
||||||
if (functionObservers.contains(fn)) {
|
if (functionObservers.contains(fn)) {
|
||||||
functionObservers.remove(fn)
|
functionObservers.remove(fn)
|
||||||
|
@ -18,7 +18,7 @@ import com.dzeio.openhealth.data.weight.WeightDao
|
|||||||
Step::class
|
Step::class
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 1,
|
||||||
exportSchema = false
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.dzeio.openhealth.data.converters
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
object TiviTypeConverters {
|
||||||
|
private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
@JvmStatic
|
||||||
|
fun toOffsetDateTime(value: String?): OffsetDateTime? {
|
||||||
|
return value?.let {
|
||||||
|
return formatter.parse(value, OffsetDateTime::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
@JvmStatic
|
||||||
|
fun fromOffsetDateTime(date: OffsetDateTime?): String? {
|
||||||
|
return date?.format(formatter)
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,65 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
package com.dzeio.openhealth.extensions
|
||||||
|
|
||||||
import android.app.Activity
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import android.content.Intent
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension Schema
|
* Extension Schema
|
||||||
*
|
*
|
||||||
* Version: 0.1.0
|
* Version: 0.2.0
|
||||||
*/
|
*/
|
||||||
interface Extension {
|
interface Extension : ActivityResultCallback<Any> {
|
||||||
|
|
||||||
data class ImportState<T>(
|
data class TaskProgress<T>(
|
||||||
val state: States = States.WIP,
|
/**
|
||||||
val list: List<T> = ArrayList()
|
* value indicating the current status of the task
|
||||||
|
*/
|
||||||
|
val state: TaskState = TaskState.INITIALIZATING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* value between 0 and 100 indicating the progress for the task
|
||||||
|
*/
|
||||||
|
val progress: Float? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additionnal message that will be displayed when the task has ended in a [TaskState.CANCELLED] or [TaskState.ERROR] state
|
||||||
|
*/
|
||||||
|
val statusMessage: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional information
|
||||||
|
*/
|
||||||
|
val additionalData: T? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class States {
|
enum class TaskState {
|
||||||
WIP,
|
/**
|
||||||
|
* define the task as being preped
|
||||||
|
*/
|
||||||
|
INITIALIZATING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the task a bein worked on
|
||||||
|
*/
|
||||||
|
WORK_IN_PROGRESS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* define the task as being done
|
||||||
|
*/
|
||||||
DONE,
|
DONE,
|
||||||
CANCELLED
|
|
||||||
|
/**
|
||||||
|
* Define the task as being cancelled
|
||||||
|
*/
|
||||||
|
CANCELLED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* define the task as being ended with an error
|
||||||
|
*/
|
||||||
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Data {
|
enum class Data {
|
||||||
@ -46,10 +84,10 @@ interface Extension {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
val permissions: Array<String>?
|
* the permissions necessary for the extension to works
|
||||||
|
*/
|
||||||
val permissionsText: String?
|
val permissions: Array<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the Source ID
|
* the Source ID
|
||||||
@ -64,51 +102,42 @@ interface Extension {
|
|||||||
val name: String
|
val name: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize hte Extension
|
* the different types of data handled by the extension
|
||||||
*
|
|
||||||
* It is run Before any functions is launched and after events handlers are set
|
|
||||||
*/
|
*/
|
||||||
fun init(activity: Activity): Array<Data>
|
val data: Array<Data>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A status shown on the extension list page
|
* Enable the extension, no code is gonna be run before
|
||||||
*/
|
*/
|
||||||
fun getStatus(): String {
|
fun enable(fragment: Fragment): Boolean
|
||||||
return "No Status set..."
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function that will check
|
|
||||||
*/
|
|
||||||
fun isAvailable(): Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return if the extension is already connected to remote of not
|
* return if the extension is already connected to remote of not
|
||||||
*/
|
*/
|
||||||
fun isConnected(): Boolean
|
suspend fun isConnected(): Boolean
|
||||||
|
|
||||||
fun connect(): LiveData<States> {
|
/**
|
||||||
return MutableLiveData(States.DONE)
|
* Return if the extension is runnable on the device
|
||||||
}
|
*/
|
||||||
|
fun isAvailable(): Boolean
|
||||||
|
|
||||||
fun importWeight(): LiveData<ImportState<Weight>> {
|
/**
|
||||||
return MutableLiveData(ImportState(States.DONE))
|
* try to connect to remote
|
||||||
}
|
*/
|
||||||
|
suspend fun connect(): Boolean
|
||||||
|
|
||||||
|
val contract: ActivityResultContract<*, *>?
|
||||||
|
val requestInput: Any?
|
||||||
|
suspend fun importWeight(): Flow<TaskProgress<ArrayList<Weight>>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* function run when outgoing sync is enabled and new value is added
|
* function run when outgoing sync is enabled and new value is added
|
||||||
* or manual export is launched
|
* or manual export is launched
|
||||||
*/
|
*/
|
||||||
fun exportWeight(weight: Weight): LiveData<States> {
|
suspend fun exportWeights(weight: Array<Weight>): Flow<TaskProgress<Unit>>
|
||||||
return MutableLiveData(States.DONE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = Unit
|
||||||
* Activity Events
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as Activity/Fragment onActivityResult
|
suspend fun permissionsGranted(): Boolean
|
||||||
*/
|
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,37 @@
|
|||||||
package com.dzeio.openhealth.extensions
|
package com.dzeio.openhealth.extensions
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
class ExtensionFactory {
|
class ExtensionFactory {
|
||||||
companion object {
|
companion object {
|
||||||
fun getExtension(extension: String): Extension? {
|
fun getExtension(extension: String): Extension? {
|
||||||
return when (extension) {
|
return when (extension) {
|
||||||
"GoogleFit" -> {
|
"GoogleFit" -> {
|
||||||
GoogleFit()
|
GoogleFitExtension()
|
||||||
|
}
|
||||||
|
"HealthConnect" -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
HealthConnectExtension()
|
||||||
|
} else {
|
||||||
|
TODO("VERSION.SDK_INT < P")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAll(): ArrayList<Extension> {
|
||||||
|
val extensions: ArrayList<Extension> = arrayListOf(
|
||||||
|
GoogleFitExtension()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
extensions.add(HealthConnectExtension())
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package com.dzeio.openhealth.extensions
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
|
|
||||||
|
class FileSystemExtension : Extension {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "FSExtension"
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var activity: Activity
|
||||||
|
|
||||||
|
override val id = "FileSystem"
|
||||||
|
override val name = "File System"
|
||||||
|
|
||||||
|
override val permissions = arrayOf(
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
override val permissionsText: String = "Please"
|
||||||
|
|
||||||
|
override fun init(activity: Activity): Array<Extension.Data> {
|
||||||
|
this.activity = activity
|
||||||
|
return Extension.Data.values()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isConnected(): Boolean = true
|
||||||
|
|
||||||
|
private val connectLiveData: MutableLiveData<Extension.States> = MutableLiveData(Extension.States.DONE)
|
||||||
|
|
||||||
|
override fun connect(): LiveData<Extension.States> = connectLiveData
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
Log.d(this.name, "[$requestCode] -> [$resultCode]: $data")
|
||||||
|
if (requestCode == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultCode == Activity.RESULT_OK) connectLiveData.value = Extension.States.DONE
|
||||||
|
// signIn(Data.values()[requestCode])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun importWeight(): LiveData<Extension.ImportState<Weight>> {
|
||||||
|
|
||||||
|
weightLiveData = MutableLiveData(
|
||||||
|
Extension.ImportState(
|
||||||
|
Extension.States.WIP
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
startImport(Extension.Data.WEIGHT)
|
||||||
|
|
||||||
|
return weightLiveData
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,8 @@ import android.Manifest
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
@ -34,8 +36,8 @@ class GoogleFit: Extension {
|
|||||||
|
|
||||||
override val permissionsText: String = "Please"
|
override val permissionsText: String = "Please"
|
||||||
|
|
||||||
override fun init(activity: Activity): Array<Extension.Data> {
|
override fun init(activity: Fragment): Array<Extension.Data> {
|
||||||
this.activity = activity
|
this.activity = activity.register
|
||||||
return arrayOf(
|
return arrayOf(
|
||||||
Extension.Data.WEIGHT
|
Extension.Data.WEIGHT
|
||||||
)
|
)
|
@ -0,0 +1,187 @@
|
|||||||
|
package com.dzeio.openhealth.extensions
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.dzeio.openhealth.core.Observable
|
||||||
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
|
import com.dzeio.openhealth.utils.PermissionsManager
|
||||||
|
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||||
|
import com.google.android.gms.fitness.Fitness
|
||||||
|
import com.google.android.gms.fitness.FitnessOptions
|
||||||
|
import com.google.android.gms.fitness.data.DataType
|
||||||
|
import com.google.android.gms.fitness.request.DataReadRequest
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class GoogleFitExtension : Extension {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "GoogleFitConnector"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val id = "GoogleFit"
|
||||||
|
override val name = "Google Fit"
|
||||||
|
|
||||||
|
override val permissions = arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
override val data: Array<Extension.Data> = arrayOf(
|
||||||
|
Extension.Data.WEIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun isConnected(): Boolean =
|
||||||
|
GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions)
|
||||||
|
|
||||||
|
|
||||||
|
private val fitnessOptions = FitnessOptions.builder()
|
||||||
|
.addDataType(DataType.TYPE_WEIGHT)
|
||||||
|
// .addDataType(DataType.TYPE_STEP_COUNT_CUMULATIVE)
|
||||||
|
// .addDataType(DataType.TYPE_CALORIES_EXPENDED)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val connectionStatus = Observable(false)
|
||||||
|
|
||||||
|
private lateinit var fragment: Fragment
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean = true
|
||||||
|
|
||||||
|
override fun enable(fragment: Fragment): Boolean {
|
||||||
|
this.fragment = fragment
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override suspend fun connect(): Boolean {
|
||||||
|
|
||||||
|
if (isConnected()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return suspendCancellableCoroutine { cancellableContinuation ->
|
||||||
|
Log.d(this.name, "Signing In")
|
||||||
|
GoogleSignIn.requestPermissions(
|
||||||
|
fragment,
|
||||||
|
124887,
|
||||||
|
getGoogleAccount(), fitnessOptions
|
||||||
|
)
|
||||||
|
connectionStatus.addOneTimeObserver { it: Boolean ->
|
||||||
|
cancellableContinuation.resume(it) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val contract: ActivityResultContract<*, Map<String, @JvmSuppressWildcards Boolean>>? = ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
override val requestInput = permissions
|
||||||
|
|
||||||
|
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(fragment.requireContext(), fitnessOptions)
|
||||||
|
|
||||||
|
private val timeRange by lazy {
|
||||||
|
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
calendar.time = Date()
|
||||||
|
val endTime = calendar.timeInMillis
|
||||||
|
|
||||||
|
// Set year to 2013 to be sure to get data from when Google Fit Started to today
|
||||||
|
calendar.set(Calendar.YEAR, 2013)
|
||||||
|
val startTime = calendar.timeInMillis
|
||||||
|
return@lazy arrayOf(startTime, endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun importWeight(): Flow<Extension.TaskProgress<ArrayList<Weight>>> =
|
||||||
|
channelFlow {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.INITIALIZATING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val type = DataType.TYPE_WEIGHT
|
||||||
|
val timeUnit = TimeUnit.MILLISECONDS
|
||||||
|
|
||||||
|
val request = DataReadRequest.Builder()
|
||||||
|
.read(type)
|
||||||
|
.setTimeRange(timeRange[0], timeRange[1], timeUnit)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
Fitness.getHistoryClient(
|
||||||
|
fragment.requireContext(),
|
||||||
|
GoogleSignIn.getAccountForExtension(fragment.requireContext(), fitnessOptions)
|
||||||
|
)
|
||||||
|
.readData(request)
|
||||||
|
.addOnSuccessListener { response ->
|
||||||
|
val weights: ArrayList<Weight> = ArrayList()
|
||||||
|
var index = 0
|
||||||
|
var total = response.dataSets.size
|
||||||
|
for (dataset in response.dataSets) {
|
||||||
|
total += dataset.dataPoints.size - 1
|
||||||
|
for (dataPoint in dataset.dataPoints) {
|
||||||
|
total += dataPoint.dataType.fields.size - 1
|
||||||
|
for (field in dataPoint.dataType.fields) {
|
||||||
|
val weight = Weight().apply {
|
||||||
|
timestamp = dataPoint.getStartTime(TimeUnit.MILLISECONDS)
|
||||||
|
weight = dataPoint.getValue(field).asFloat()
|
||||||
|
source = this@GoogleFitExtension.id
|
||||||
|
}
|
||||||
|
weights.add(weight)
|
||||||
|
runBlocking {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.WORK_IN_PROGRESS,
|
||||||
|
progress = index++ / total.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runBlocking {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.DONE,
|
||||||
|
additionalData = weights
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
runBlocking {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.ERROR,
|
||||||
|
statusMessage = it.localizedMessage ?: it.message ?: "Unknown error"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exportWeights(weight: Array<Weight>): Flow<Extension.TaskProgress<Unit>> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun permissionsGranted(): Boolean {
|
||||||
|
return PermissionsManager.hasPermission(this.fragment.requireContext(), permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(result: Any) {
|
||||||
|
if ((result as Map<*, *>).containsValue(false)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionStatus.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package com.dzeio.openhealth.extensions
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.health.connect.client.HealthConnectClient
|
||||||
|
import androidx.health.connect.client.PermissionController
|
||||||
|
import androidx.health.connect.client.permission.HealthPermission
|
||||||
|
import androidx.health.connect.client.records.HeartRateRecord
|
||||||
|
import androidx.health.connect.client.records.StepsRecord
|
||||||
|
import androidx.health.connect.client.records.WeightRecord
|
||||||
|
import androidx.health.connect.client.request.ReadRecordsRequest
|
||||||
|
import androidx.health.connect.client.time.TimeRangeFilter
|
||||||
|
import com.dzeio.openhealth.core.Observable
|
||||||
|
import com.dzeio.openhealth.data.weight.Weight
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
class HealthConnectExtension : Extension {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "HealthConnectExtension"
|
||||||
|
}
|
||||||
|
|
||||||
|
// build a set of permissions for required data types
|
||||||
|
val PERMISSIONS =
|
||||||
|
setOf(
|
||||||
|
HealthPermission.createReadPermission(HeartRateRecord::class),
|
||||||
|
HealthPermission.createWritePermission(HeartRateRecord::class),
|
||||||
|
HealthPermission.createReadPermission(StepsRecord::class),
|
||||||
|
HealthPermission.createWritePermission(StepsRecord::class)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
override val id = "HealthConnect"
|
||||||
|
override val name = "Health Connect"
|
||||||
|
|
||||||
|
override val permissions = arrayOf(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
override val requestInput = PERMISSIONS
|
||||||
|
|
||||||
|
override val data: Array<Extension.Data> = arrayOf(
|
||||||
|
Extension.Data.WEIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun isConnected(): Boolean = true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private val connectionStatus = Observable(false)
|
||||||
|
|
||||||
|
private lateinit var fragment: Fragment
|
||||||
|
private lateinit var client: HealthConnectClient
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return HealthConnectClient.isAvailable(fragment.requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enable(fragment: Fragment): Boolean {
|
||||||
|
this.fragment = fragment
|
||||||
|
if (!isAvailable()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = HealthConnectClient.getOrCreate(fragment.requireContext())
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun connect(): Boolean = true
|
||||||
|
|
||||||
|
override suspend fun importWeight(): Flow<Extension.TaskProgress<ArrayList<Weight>>> =
|
||||||
|
channelFlow {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.INITIALIZATING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = client.readRecords(
|
||||||
|
ReadRecordsRequest(
|
||||||
|
WeightRecord::class,
|
||||||
|
timeRangeFilter = TimeRangeFilter.before(Instant.now())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val weights: ArrayList<Weight> = ArrayList()
|
||||||
|
var index = 0
|
||||||
|
for (record in response.records) {
|
||||||
|
val weight = Weight().apply {
|
||||||
|
timestamp = record.time.toEpochMilli()
|
||||||
|
weight = record.weight.inKilograms.toFloat()
|
||||||
|
source = this@HealthConnectExtension.id
|
||||||
|
}
|
||||||
|
weights.add(weight)
|
||||||
|
runBlocking {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.WORK_IN_PROGRESS,
|
||||||
|
progress = index++ / response.records.size.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runBlocking {
|
||||||
|
send(
|
||||||
|
Extension.TaskProgress(
|
||||||
|
Extension.TaskState.DONE,
|
||||||
|
additionalData = weights
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun exportWeights(weight: Array<Weight>): Flow<Extension.TaskProgress<Unit>> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(result: Any) {
|
||||||
|
if ((result as Set<*>).containsAll(this.PERMISSIONS)) connectionStatus.value = true
|
||||||
|
// signIn(Data.values()[requestCode])
|
||||||
|
}
|
||||||
|
|
||||||
|
override val contract: ActivityResultContract<Set<HealthPermission>, Set<HealthPermission>>
|
||||||
|
get() = PermissionController.createRequestPermissionResultContract()
|
||||||
|
|
||||||
|
override suspend fun permissionsGranted(): Boolean {
|
||||||
|
return this.client.permissionController.getGrantedPermissions(this.PERMISSIONS).containsAll(this.PERMISSIONS)
|
||||||
|
}
|
||||||
|
}
|
@ -101,7 +101,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val navHostFragment =
|
val navHostFragment =
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.dzeio.openhealth.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import com.dzeio.openhealth.core.BaseActivity
|
||||||
|
import com.dzeio.openhealth.databinding.ActivityPrivacyPolicyBinding
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PrivacyPolicyActivity : BaseActivity<ActivityPrivacyPolicyBinding>() {
|
||||||
|
|
||||||
|
override val bindingInflater: (LayoutInflater) -> ActivityPrivacyPolicyBinding =
|
||||||
|
ActivityPrivacyPolicyBinding::inflate
|
||||||
|
}
|
@ -60,7 +60,8 @@ class BrowseFragment :
|
|||||||
Manifest.permission.ACTIVITY_RECOGNITION
|
Manifest.permission.ACTIVITY_RECOGNITION
|
||||||
)
|
)
|
||||||
|
|
||||||
val notificationPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionsManager.hasPermission(
|
val notificationPermission =
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && PermissionsManager.hasPermission(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
)
|
)
|
||||||
@ -91,10 +92,13 @@ class BrowseFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.weight.observe(viewLifecycleOwner) {
|
viewModel.weight.observe(viewLifecycleOwner) {
|
||||||
binding.weightText.setText(String.format(
|
binding.weightText.setText(
|
||||||
|
String.format(
|
||||||
resources.getString(R.string.weight_current),
|
resources.getString(R.string.weight_current),
|
||||||
it
|
it,
|
||||||
))
|
resources.getString(R.string.unit_mass_kilogram_unit)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package com.dzeio.openhealth.ui.extension
|
package com.dzeio.openhealth.ui.extension
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.dzeio.openhealth.core.BaseFragment
|
import com.dzeio.openhealth.core.BaseFragment
|
||||||
@ -13,7 +16,8 @@ import com.dzeio.openhealth.databinding.FragmentExtensionBinding
|
|||||||
import com.dzeio.openhealth.extensions.Extension
|
import com.dzeio.openhealth.extensions.Extension
|
||||||
import com.dzeio.openhealth.extensions.ExtensionFactory
|
import com.dzeio.openhealth.extensions.ExtensionFactory
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.lang.Exception
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ExtensionFragment :
|
class ExtensionFragment :
|
||||||
@ -24,29 +28,49 @@ class ExtensionFragment :
|
|||||||
|
|
||||||
private val args: ExtensionFragmentArgs by navArgs()
|
private val args: ExtensionFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
private val extension by lazy {
|
||||||
|
ExtensionFactory.getExtension(args.extension)
|
||||||
|
?: throw Exception("No Extension found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var request: ActivityResultLauncher<Any>? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
if (this.extension.contract != null) {
|
||||||
|
this.request =
|
||||||
|
registerForActivityResult<Any, Any>(this.extension.contract!! as ActivityResultContract<Any, Any>) {
|
||||||
|
this.extension.onActivityResult(it as Any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onAttach(context)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val extension = ExtensionFactory.getExtension(args.extension)
|
val enabled = extension.enable(this)
|
||||||
?: throw Exception("No Extension found!")
|
|
||||||
|
if (!enabled) {
|
||||||
|
throw Exception("Extension can't be enabled (${extension.id})")
|
||||||
|
}
|
||||||
|
|
||||||
requireActivity().actionBar?.title = extension.name
|
requireActivity().actionBar?.title = extension.name
|
||||||
|
|
||||||
extension.init(requireActivity())
|
// extension.init(requireActivity())
|
||||||
|
|
||||||
binding.importButton.setOnClickListener {
|
binding.importButton.setOnClickListener {
|
||||||
val dialog = ProgressDialog(requireContext())
|
val dialog = ProgressDialog(requireContext())
|
||||||
dialog.setTitle("Importing...")
|
dialog.setTitle("Importing...")
|
||||||
dialog.setMessage("Imported 0 values")
|
dialog.setMessage("Imported 0 values")
|
||||||
dialog.show()
|
dialog.show()
|
||||||
val data = extension.importWeight()
|
lifecycleScope.launch {
|
||||||
data.observe(viewLifecycleOwner) { state ->
|
extension.importWeight().collectLatest { state ->
|
||||||
Log.d("ExtensionFragment", state.state.name)
|
Log.d("ExtensionFragment", state.state.name)
|
||||||
Log.d("ExtensionFragment", state.list.size.toString())
|
dialog.setMessage(state.statusMessage ?: "progress ${state.progress}%")
|
||||||
dialog.setMessage("Imported ${state.list.size} values")
|
if (state.state == Extension.TaskState.DONE) {
|
||||||
if (state.state == Extension.States.DONE) {
|
|
||||||
dialog.setMessage("Finishing Import...")
|
dialog.setMessage("Finishing Import...")
|
||||||
lifecycleScope.launchWhenStarted {
|
lifecycleScope.launchWhenStarted {
|
||||||
state.list.forEach {
|
state.additionalData!!.forEach {
|
||||||
it.source = extension.id
|
it.source = extension.id
|
||||||
viewModel.importWeight(it)
|
viewModel.importWeight(it)
|
||||||
}
|
}
|
||||||
@ -56,4 +80,11 @@ class ExtensionFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
if (!extension.permissionsGranted() && request != null) {
|
||||||
|
request!!.launch(extension.requestInput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
package com.dzeio.openhealth.ui.extensions
|
package com.dzeio.openhealth.ui.extensions
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.dzeio.openhealth.R
|
|
||||||
import com.dzeio.openhealth.adapters.ExtensionAdapter
|
import com.dzeio.openhealth.adapters.ExtensionAdapter
|
||||||
import com.dzeio.openhealth.core.BaseFragment
|
import com.dzeio.openhealth.core.BaseFragment
|
||||||
import com.dzeio.openhealth.databinding.FragmentExtensionsBinding
|
import com.dzeio.openhealth.databinding.FragmentExtensionsBinding
|
||||||
import com.dzeio.openhealth.extensions.Extension
|
import com.dzeio.openhealth.extensions.Extension
|
||||||
import com.dzeio.openhealth.extensions.GoogleFit
|
|
||||||
import com.dzeio.openhealth.utils.PermissionsManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ExtensionsFragment :
|
class ExtensionsFragment :
|
||||||
@ -32,18 +28,6 @@ class ExtensionsFragment :
|
|||||||
|
|
||||||
private lateinit var activeExtension: Extension
|
private lateinit var activeExtension: Extension
|
||||||
|
|
||||||
private val activityResult = registerForActivityResult(
|
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
|
||||||
) { map ->
|
|
||||||
if (map.containsValue(false)) {
|
|
||||||
// TODO: Show a popup with choice to change it
|
|
||||||
Toast.makeText(requireContext(), R.string.permission_declined, Toast.LENGTH_LONG).show()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionIsConnected(activeExtension)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
@ -52,62 +36,38 @@ class ExtensionsFragment :
|
|||||||
val manager = LinearLayoutManager(requireContext())
|
val manager = LinearLayoutManager(requireContext())
|
||||||
recycler.layoutManager = manager
|
recycler.layoutManager = manager
|
||||||
|
|
||||||
val adapter = ExtensionAdapter()
|
val adapter = ExtensionAdapter(viewModel.config)
|
||||||
adapter.onItemClick = {
|
adapter.onItemClick = {
|
||||||
activeExtension = it
|
activeExtension = it
|
||||||
|
activeExtension.enable(this)
|
||||||
Log.d(TAG, "${it.id}: ${it.name}")
|
Log.d(TAG, "${it.id}: ${it.name}")
|
||||||
|
|
||||||
this.extensionPermissionsVerified(it)
|
lifecycleScope.launch {
|
||||||
|
extensionIsConnected(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
|
|
||||||
val list = arrayOf(
|
val list = viewModel.extensions
|
||||||
GoogleFit()
|
|
||||||
).toList()
|
|
||||||
|
|
||||||
list.forEach {
|
list.forEach {
|
||||||
it.init(requireActivity())
|
it.enable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.set(list)
|
adapter.set(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
private suspend fun extensionIsConnected(it: Extension) {
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
activeExtension.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extensionPermissionsVerified(it: Extension) {
|
|
||||||
// Check for the extension permissions
|
|
||||||
if (it.permissions != null && !PermissionsManager.hasPermission(
|
|
||||||
requireContext(),
|
|
||||||
it.permissions!!
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// TODO: show popup explaining the permissions requested
|
|
||||||
|
|
||||||
// show permissions
|
|
||||||
activityResult.launch(it.permissions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
extensionIsConnected(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extensionIsConnected(it: Extension) {
|
|
||||||
// check if it is connected
|
// check if it is connected
|
||||||
if (it.isConnected()) {
|
if (it.isConnected()) {
|
||||||
gotoExtension(it)
|
gotoExtension(it)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDK: maybe give less liberty to the extension IDK
|
|
||||||
val ld = it.connect()
|
val ld = it.connect()
|
||||||
ld.observe(viewLifecycleOwner) { state ->
|
if (ld) {
|
||||||
Log.d(TAG, state.toString())
|
|
||||||
if (state == Extension.States.DONE) {
|
|
||||||
gotoExtension(it)
|
gotoExtension(it)
|
||||||
}
|
}
|
||||||
}
|
// handle if extension can't be connected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun gotoExtension(it: Extension) {
|
private fun gotoExtension(it: Extension) {
|
||||||
|
@ -1,31 +1,16 @@
|
|||||||
package com.dzeio.openhealth.ui.extensions
|
package com.dzeio.openhealth.ui.extensions
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.dzeio.openhealth.core.BaseViewModel
|
import com.dzeio.openhealth.core.BaseViewModel
|
||||||
import com.dzeio.openhealth.data.weight.Weight
|
import com.dzeio.openhealth.extensions.ExtensionFactory
|
||||||
import com.dzeio.openhealth.data.weight.WeightRepository
|
import com.dzeio.openhealth.utils.Configuration
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ExtensionsViewModel @Inject internal constructor(
|
class ExtensionsViewModel @Inject internal constructor(
|
||||||
private val weightRepository: WeightRepository
|
val config: Configuration
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val text = MutableLiveData<String>().apply {
|
val extensions = ExtensionFactory.getAll()
|
||||||
value = "This is slideshow Fragment"
|
|
||||||
}
|
|
||||||
val importProgress = MutableLiveData<Int>().apply {
|
|
||||||
value = 0
|
|
||||||
}
|
|
||||||
// If -1 progress is undetermined
|
|
||||||
// If 0 no progress bar
|
|
||||||
// Else progress bar
|
|
||||||
val importProgressTotal = MutableLiveData<Int>().apply {
|
|
||||||
value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun importWeight(weight: Weight) = weightRepository.addWeight(weight)
|
|
||||||
suspend fun deleteFromSource(source: String) = weightRepository.deleteFromSource(source)
|
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ import com.dzeio.openhealth.data.weight.Weight
|
|||||||
import com.dzeio.openhealth.databinding.FragmentHomeBinding
|
import com.dzeio.openhealth.databinding.FragmentHomeBinding
|
||||||
import com.dzeio.openhealth.graphs.WeightChart
|
import com.dzeio.openhealth.graphs.WeightChart
|
||||||
import com.dzeio.openhealth.ui.weight.WeightDialog
|
import com.dzeio.openhealth.ui.weight.WeightDialog
|
||||||
import com.dzeio.openhealth.units.UnitFactory
|
import com.dzeio.openhealth.units.Units
|
||||||
import com.dzeio.openhealth.utils.DrawUtils
|
import com.dzeio.openhealth.utils.DrawUtils
|
||||||
import com.dzeio.openhealth.utils.GraphUtils
|
import com.dzeio.openhealth.utils.GraphUtils
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
@ -56,7 +56,7 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val waterUnit =
|
val waterUnit =
|
||||||
UnitFactory.volume(settings.getString("water_unit", "milliliter") ?: "Milliliter")
|
Units.Volume.find(settings.getString("water_unit", "milliliter") ?: "Milliliter")
|
||||||
|
|
||||||
binding.fragmentHomeWaterTotal.text =
|
binding.fragmentHomeWaterTotal.text =
|
||||||
String.format(
|
String.format(
|
||||||
@ -147,7 +147,7 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
|||||||
private fun updateWater(newValue: Int) {
|
private fun updateWater(newValue: Int) {
|
||||||
|
|
||||||
val waterUnit =
|
val waterUnit =
|
||||||
UnitFactory.volume(settings.getString("water_unit", "milliliter") ?: "Milliliter")
|
Units.Volume.find(settings.getString("water_unit", "milliliter") ?: "Milliliter")
|
||||||
|
|
||||||
binding.fragmentHomeWaterCurrent.text =
|
binding.fragmentHomeWaterCurrent.text =
|
||||||
String.format(
|
String.format(
|
||||||
|
@ -14,8 +14,10 @@ import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding
|
|||||||
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
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class StepsHomeFragment :
|
class StepsHomeFragment :
|
||||||
@ -104,6 +106,17 @@ class StepsHomeFragment :
|
|||||||
viewModel.items.observe(viewLifecycleOwner) { list ->
|
viewModel.items.observe(viewLifecycleOwner) { list ->
|
||||||
adapter.set(list)
|
adapter.set(list)
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return@observe
|
||||||
|
}
|
||||||
|
|
||||||
|
val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
|
||||||
|
cal.set(Calendar.HOUR, 0)
|
||||||
|
cal.set(Calendar.MINUTE, 0)
|
||||||
|
cal.set(Calendar.SECOND, 0)
|
||||||
|
cal.set(Calendar.MILLISECOND, 0)
|
||||||
|
|
||||||
// chart.animation.enabled = false
|
// chart.animation.enabled = false
|
||||||
// chart.animation.refreshRate = 60
|
// chart.animation.refreshRate = 60
|
||||||
// chart.animation.duration = 300
|
// chart.animation.duration = 300
|
||||||
|
@ -113,10 +113,13 @@ class ListWeightFragment :
|
|||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.action_add -> {
|
R.id.action_add -> {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
ListWeightFragmentDirections.actionNavListWeightToNavAddWeightDialog()
|
ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog(
|
||||||
|
WeightDialog.DialogTypes.ADD_WEIGHT.ordinal
|
||||||
|
)
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
app/src/main/res/layout/activity_privacy_policy.xml
Normal file
18
app/src/main/res/layout/activity_privacy_policy.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:fitsSystemWindows="false"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView5"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="We do not log nothing about you nor send it without your permission to anybody."
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_extensions"
|
android:id="@+id/nav_extensions"
|
||||||
|
android:enabled="false"
|
||||||
android:title="@string/menu_extensions"
|
android:title="@string/menu_extensions"
|
||||||
android:icon="@drawable/ic_outline_account_circle_24"/>
|
android:icon="@drawable/ic_outline_account_circle_24"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
9
app/src/main/res/values/health_permissions.xml
Normal file
9
app/src/main/res/values/health_permissions.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<array name="health_permissions">
|
||||||
|
<item>androidx.health.permission.HeartRate.READ</item>
|
||||||
|
<item>androidx.health.permission.HeartRate.WRITE</item>
|
||||||
|
<item>androidx.health.permission.Steps.READ</item>
|
||||||
|
<item>androidx.health.permission.Steps.WRITE</item>
|
||||||
|
</array>
|
||||||
|
</resources>
|
@ -13,11 +13,11 @@
|
|||||||
<!-- Units -->
|
<!-- Units -->
|
||||||
<string name="unit_mass_kilogram_name_singular">Kilogram</string>
|
<string name="unit_mass_kilogram_name_singular">Kilogram</string>
|
||||||
<string name="unit_mass_kilogram_name_plural">Kilograms</string>
|
<string name="unit_mass_kilogram_name_plural">Kilograms</string>
|
||||||
<string name="unit_mass_kilogram_unit" translatable="false">%1$skg</string>
|
<string name="unit_mass_kilogram_unit" translatable="false">kg</string>
|
||||||
|
|
||||||
<string name="unit_mass_pound_name_singular">Pound</string>
|
<string name="unit_mass_pound_name_singular">Pound</string>
|
||||||
<string name="unit_mass_pound_name_plural">Pounds</string>
|
<string name="unit_mass_pound_name_plural">Pounds</string>
|
||||||
<string name="unit_mass_pound_unit" translatable="false">%1$slbs</string>
|
<string name="unit_mass_pound_unit" translatable="false">lbs</string>
|
||||||
|
|
||||||
<string name="unit_volume_milliliter_name_singular">Milliliter</string>
|
<string name="unit_volume_milliliter_name_singular">Milliliter</string>
|
||||||
<string name="unit_volume_milliliter_name_plural">Milliliters</string>
|
<string name="unit_volume_milliliter_name_plural">Milliliters</string>
|
||||||
|
@ -4,7 +4,7 @@ buildscript {
|
|||||||
classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.5")
|
classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.5")
|
||||||
|
|
||||||
// Safe Navigation
|
// Safe Navigation
|
||||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.1")
|
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.5")
|
classpath("com.google.android.gms:oss-licenses-plugin:0.10.5")
|
||||||
@ -12,9 +12,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "7.2.2" apply false
|
id("com.android.application") version "7.3.1" apply false
|
||||||
id("com.android.library") version "7.2.2" apply false
|
id("com.android.library") version "7.3.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.6.10" apply false
|
id("org.jetbrains.kotlin.android") version "1.6.21" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
task("clean") {
|
task("clean") {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user