1
0
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:
Florian Bouillon 2022-11-23 22:34:30 +01:00
parent a9a488b122
commit 7b3f409733
Signed by: Florian Bouillon
GPG Key ID: BEEAF3722D0EBF64
30 changed files with 903 additions and 183 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,7 +101,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} }
} }
} }
} }
val navHostFragment = val navHostFragment =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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