diff --git a/README.md b/README.md index 7e0e2e6..803aa92 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,15 @@ ## 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: -| Permission | Why is it requested | -| :--------------------: |:-----------------------------------------------------------------| -| ACCESS_FINE_LOCATION | Google Fit Extension Requirement (maybe not, still have to test) | +| Permission | Why is it requested | +|:----------------------:|:-----------------------------------------------------------------| +| ACCESS_FINE_LOCATION | Google Fit Extension Requirement (maybe not, still have to test) | | ACCESS_COARSE_LOCATION | Same as above | -| ACTIVITY_RECOGNITION | Device Steps Usage | +| ACTIVITY_RECOGNITION | Device Steps Usage | No other permissions are used (even the internet permission ;)). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3b3c49e..ecd96f1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,6 +15,14 @@ plugins { kotlin("kapt") } +val appID = "com.dzeio.openhealth" + +// Languages +val locales = listOf("en", "fr") + +val sdkMin = 21 +val sdkTarget = 33 + android { signingConfigs { @@ -37,17 +45,17 @@ android { } } - compileSdk = 33 + compileSdk = sdkTarget defaultConfig { // App ID - applicationId = "com.dzeio.openhealth" + applicationId = appID // Android 5 Lollipop - minSdk = 21 + minSdk = sdkMin // Android 12 - targetSdk = 33 + targetSdk = sdkTarget // Semantic Versioning versionName = "1.0.0" @@ -55,8 +63,11 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - // Languages - val locales = listOf("en", "fr") + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + } + } buildConfigField( "String[]", @@ -104,7 +115,7 @@ android { viewBinding = true dataBinding = true } - namespace = "com.dzeio.openhealth" + namespace = appID } dependencies { @@ -115,10 +126,10 @@ dependencies { implementation("com.dzeio:crashhandler:1.0.1") // Core dependencies - implementation("androidx.core:core-ktx:1.8.0") - implementation("androidx.appcompat:appcompat:1.6.0-beta01") + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.7.0-alpha01") implementation("javax.inject:javax.inject:1") - implementation("com.google.android.material:material:1.7.0-beta01") + implementation("com.google.android.material:material:1.8.0-alpha02") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") @@ -135,8 +146,8 @@ dependencies { implementation("androidx.datastore:datastore:1.0.0") // Navigation - implementation("androidx.navigation:navigation-fragment-ktx:2.5.1") - implementation("androidx.navigation:navigation-ui-ktx:2.5.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") + implementation("androidx.navigation:navigation-ui-ktx:2.5.3") // Paging implementation("androidx.paging:paging-runtime:3.1.1") @@ -148,8 +159,8 @@ dependencies { // Tests testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.3") - androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") // Graph implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") @@ -163,7 +174,8 @@ dependencies { // Google Fit 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 implementation(files("libs/samsung-health-data-1.5.0.aar")) diff --git a/app/schemas/com.dzeio.openhealth.data.AppDatabase/1.json b/app/schemas/com.dzeio.openhealth.data.AppDatabase/1.json new file mode 100644 index 0000000..ef5c56a --- /dev/null +++ b/app/schemas/com.dzeio.openhealth.data.AppDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7aeb020..22b0177 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + @@ -16,6 +16,7 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/dzeio/openhealth/Settings.kt b/app/src/main/java/com/dzeio/openhealth/Settings.kt index 8b4b0d8..ec50814 100644 --- a/app/src/main/java/com/dzeio/openhealth/Settings.kt +++ b/app/src/main/java/com/dzeio/openhealth/Settings.kt @@ -1,5 +1,7 @@ package com.dzeio.openhealth +import com.dzeio.openhealth.extensions.Extension + object Settings { /** @@ -27,4 +29,8 @@ object Settings { */ const val MASS_UNIT = "com.dzeio.open-health.unit.mass" + fun extensionEnabled(extension: Extension): String { + return "com.dzeio.open-health.extension.${extension.id}.enabled" + } + } diff --git a/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt b/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt index 6ad8ef7..14a6afb 100644 --- a/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt +++ b/app/src/main/java/com/dzeio/openhealth/adapters/ExtensionAdapter.kt @@ -2,13 +2,17 @@ package com.dzeio.openhealth.adapters import android.view.LayoutInflater import android.view.ViewGroup +import com.dzeio.openhealth.Settings import com.dzeio.openhealth.core.BaseAdapter import com.dzeio.openhealth.core.BaseViewHolder import com.dzeio.openhealth.databinding.LayoutExtensionItemBinding import com.dzeio.openhealth.extensions.Extension +import com.dzeio.openhealth.utils.Configuration -class ExtensionAdapter : BaseAdapter() { +class ExtensionAdapter( + private val config: Configuration +) : BaseAdapter() { override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutExtensionItemBinding = LayoutExtensionItemBinding::inflate @@ -20,10 +24,15 @@ class ExtensionAdapter : BaseAdapter() { item: Extension, position: Int ) { + val isEnabled = config.getBoolean(Settings.extensionEnabled(item)).value ?: false holder.binding.name.text = item.name - holder.binding.status.text = item.getStatus() - holder.binding.card.setOnClickListener { - onItemClick?.invoke(item) + holder.binding.card.isClickable = item.isAvailable() + holder.binding.card.isEnabled = item.isAvailable() + holder.binding.status.text = "enabled = $isEnabled" + if (item.isAvailable()) { + holder.binding.card.setOnClickListener { + onItemClick?.invoke(item) + } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dzeio/openhealth/core/BaseStaticFragment.kt b/app/src/main/java/com/dzeio/openhealth/core/BaseStaticFragment.kt index a97cbeb..0e2e17d 100644 --- a/app/src/main/java/com/dzeio/openhealth/core/BaseStaticFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/core/BaseStaticFragment.kt @@ -1,5 +1,6 @@ package com.dzeio.openhealth.core +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/com/dzeio/openhealth/core/Observable.kt b/app/src/main/java/com/dzeio/openhealth/core/Observable.kt index a94a2b2..5b1f872 100644 --- a/app/src/main/java/com/dzeio/openhealth/core/Observable.kt +++ b/app/src/main/java/com/dzeio/openhealth/core/Observable.kt @@ -14,6 +14,14 @@ open class Observable(baseValue: T) { } } + fun addOneTimeObserver(fn: (T) -> Unit) { + val index = functionObservers.size + functionObservers.add { + fn(it) + functionObservers.removeAt(index) + } + } + fun removeObserver(fn: (T) -> Unit) { if (functionObservers.contains(fn)) { functionObservers.remove(fn) diff --git a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt index 96fb723..8d3adf0 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt @@ -18,7 +18,7 @@ import com.dzeio.openhealth.data.weight.WeightDao Step::class ], version = 1, - exportSchema = false + exportSchema = true ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/dzeio/openhealth/data/converters/OffsetDateTimeConverter.kt b/app/src/main/java/com/dzeio/openhealth/data/converters/OffsetDateTimeConverter.kt new file mode 100644 index 0000000..ca107c2 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/data/converters/OffsetDateTimeConverter.kt @@ -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) + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt b/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt index ccce665..fd45fec 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt @@ -1,27 +1,65 @@ package com.dzeio.openhealth.extensions -import android.app.Activity -import android.content.Intent -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.contract.ActivityResultContract +import androidx.fragment.app.Fragment import com.dzeio.openhealth.data.weight.Weight +import kotlinx.coroutines.flow.Flow /** * Extension Schema * - * Version: 0.1.0 + * Version: 0.2.0 */ -interface Extension { +interface Extension : ActivityResultCallback { - data class ImportState( - val state: States = States.WIP, - val list: List = ArrayList() + data class TaskProgress( + /** + * 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 { - WIP, + enum class TaskState { + /** + * define the task as being preped + */ + INITIALIZATING, + + /** + * Define the task a bein worked on + */ + WORK_IN_PROGRESS, + + /** + * define the task as being done + */ DONE, - CANCELLED + + /** + * Define the task as being cancelled + */ + CANCELLED, + + /** + * define the task as being ended with an error + */ + ERROR } enum class Data { @@ -46,10 +84,10 @@ interface Extension { */ } - - val permissions: Array? - - val permissionsText: String? + /** + * the permissions necessary for the extension to works + */ + val permissions: Array /** * the Source ID @@ -64,51 +102,42 @@ interface Extension { val name: String /** - * Initialize hte Extension - * - * It is run Before any functions is launched and after events handlers are set + * the different types of data handled by the extension */ - fun init(activity: Activity): Array + val data: Array /** - * A status shown on the extension list page + * Enable the extension, no code is gonna be run before */ - fun getStatus(): String { - return "No Status set..." - } - - /** - * Function that will check - */ - fun isAvailable(): Boolean + fun enable(fragment: Fragment): Boolean /** * return if the extension is already connected to remote of not */ - fun isConnected(): Boolean + suspend fun isConnected(): Boolean - fun connect(): LiveData { - return MutableLiveData(States.DONE) - } + /** + * Return if the extension is runnable on the device + */ + fun isAvailable(): Boolean - fun importWeight(): LiveData> { - return MutableLiveData(ImportState(States.DONE)) - } + /** + * try to connect to remote + */ + suspend fun connect(): Boolean + + val contract: ActivityResultContract<*, *>? + val requestInput: Any? + suspend fun importWeight(): Flow>> /** * function run when outgoing sync is enabled and new value is added * or manual export is launched */ - fun exportWeight(weight: Weight): LiveData { - return MutableLiveData(States.DONE) - } + suspend fun exportWeights(weight: Array): Flow> - /** - * Activity Events - */ +// fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = Unit - /** - * Same as Activity/Fragment onActivityResult - */ - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} + + suspend fun permissionsGranted(): Boolean } diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt b/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt index 19c6f6d..df4b895 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/ExtensionFactory.kt @@ -1,16 +1,37 @@ package com.dzeio.openhealth.extensions +import android.os.Build + class ExtensionFactory { companion object { fun getExtension(extension: String): Extension? { return when (extension) { "GoogleFit" -> { - GoogleFit() + GoogleFitExtension() + } + "HealthConnect" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + HealthConnectExtension() + } else { + TODO("VERSION.SDK_INT < P") + } } else -> { null } } } + + fun getAll(): ArrayList { + val extensions: ArrayList = arrayListOf( + GoogleFitExtension() + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + extensions.add(HealthConnectExtension()) + } + + return extensions + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/FileSystemExtension.kt.old b/app/src/main/java/com/dzeio/openhealth/extensions/FileSystemExtension.kt.old new file mode 100644 index 0000000..c5e6ec0 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/extensions/FileSystemExtension.kt.old @@ -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 { + 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 = MutableLiveData(Extension.States.DONE) + + override fun connect(): LiveData = 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> { + + weightLiveData = MutableLiveData( + Extension.ImportState( + Extension.States.WIP + ) + ) + + startImport(Extension.Data.WEIGHT) + + return weightLiveData + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt.old similarity index 97% rename from app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt rename to app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt.old index 9585045..e5b5d40 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt.old @@ -4,6 +4,8 @@ import android.Manifest import android.app.Activity import android.content.Intent import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.dzeio.openhealth.data.weight.Weight @@ -34,8 +36,8 @@ class GoogleFit: Extension { override val permissionsText: String = "Please" - override fun init(activity: Activity): Array { - this.activity = activity + override fun init(activity: Fragment): Array { + this.activity = activity.register return arrayOf( Extension.Data.WEIGHT ) diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFitExtension.kt b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFitExtension.kt new file mode 100644 index 0000000..a347645 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFitExtension.kt @@ -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 = 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>? = 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>> = + 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 = 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): Flow> { + 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 + } + +} diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/HealthConnectExtension.kt b/app/src/main/java/com/dzeio/openhealth/extensions/HealthConnectExtension.kt new file mode 100644 index 0000000..808764e --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/extensions/HealthConnectExtension.kt @@ -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 = 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>> = + channelFlow { + send( + Extension.TaskProgress( + Extension.TaskState.INITIALIZATING + ) + ) + + val response = client.readRecords( + ReadRecordsRequest( + WeightRecord::class, + timeRangeFilter = TimeRangeFilter.before(Instant.now()) + ) + ) + + val weights: ArrayList = 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): Flow> { + 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> + get() = PermissionController.createRequestPermissionResultContract() + + override suspend fun permissionsGranted(): Boolean { + return this.client.permissionController.getGrantedPermissions(this.PERMISSIONS).containsAll(this.PERMISSIONS) + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt b/app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt index 302b907..20d198c 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt @@ -101,7 +101,6 @@ class MainActivity : BaseActivity() { } } } - } val navHostFragment = diff --git a/app/src/main/java/com/dzeio/openhealth/ui/PrivacyPolicyActivity.kt b/app/src/main/java/com/dzeio/openhealth/ui/PrivacyPolicyActivity.kt new file mode 100644 index 0000000..30229bb --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/PrivacyPolicyActivity.kt @@ -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() { + + override val bindingInflater: (LayoutInflater) -> ActivityPrivacyPolicyBinding = + ActivityPrivacyPolicyBinding::inflate +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/browse/BrowseFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/browse/BrowseFragment.kt index ca334cc..998f765 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/browse/BrowseFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/browse/BrowseFragment.kt @@ -60,7 +60,8 @@ class BrowseFragment : 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(), Manifest.permission.POST_NOTIFICATIONS ) @@ -91,10 +92,13 @@ class BrowseFragment : } viewModel.weight.observe(viewLifecycleOwner) { - binding.weightText.setText(String.format( - resources.getString(R.string.weight_current), - it - )) + binding.weightText.setText( + String.format( + resources.getString(R.string.weight_current), + it, + resources.getString(R.string.unit_mass_kilogram_unit) + ) + ) } } diff --git a/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt index be363b9..c383d8d 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/extension/ExtensionFragment.kt @@ -1,11 +1,14 @@ package com.dzeio.openhealth.ui.extension import android.app.ProgressDialog +import android.content.Context import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import com.dzeio.openhealth.core.BaseFragment @@ -13,7 +16,8 @@ import com.dzeio.openhealth.databinding.FragmentExtensionBinding import com.dzeio.openhealth.extensions.Extension import com.dzeio.openhealth.extensions.ExtensionFactory import dagger.hilt.android.AndroidEntryPoint -import java.lang.Exception +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch @AndroidEntryPoint class ExtensionFragment : @@ -24,36 +28,63 @@ class ExtensionFragment : private val args: ExtensionFragmentArgs by navArgs() + private val extension by lazy { + ExtensionFactory.getExtension(args.extension) + ?: throw Exception("No Extension found!") + } + + private var request: ActivityResultLauncher? = null + + override fun onAttach(context: Context) { + if (this.extension.contract != null) { + this.request = + registerForActivityResult(this.extension.contract!! as ActivityResultContract) { + this.extension.onActivityResult(it as Any) + } + } + super.onAttach(context) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val extension = ExtensionFactory.getExtension(args.extension) - ?: throw Exception("No Extension found!") + val enabled = extension.enable(this) + + if (!enabled) { + throw Exception("Extension can't be enabled (${extension.id})") + } + requireActivity().actionBar?.title = extension.name - extension.init(requireActivity()) +// extension.init(requireActivity()) binding.importButton.setOnClickListener { val dialog = ProgressDialog(requireContext()) dialog.setTitle("Importing...") dialog.setMessage("Imported 0 values") dialog.show() - val data = extension.importWeight() - data.observe(viewLifecycleOwner) { state -> - Log.d("ExtensionFragment", state.state.name) - Log.d("ExtensionFragment", state.list.size.toString()) - dialog.setMessage("Imported ${state.list.size} values") - if (state.state == Extension.States.DONE) { - dialog.setMessage("Finishing Import...") - lifecycleScope.launchWhenStarted { - state.list.forEach { - it.source = extension.id - viewModel.importWeight(it) + lifecycleScope.launch { + extension.importWeight().collectLatest { state -> + Log.d("ExtensionFragment", state.state.name) + dialog.setMessage(state.statusMessage ?: "progress ${state.progress}%") + if (state.state == Extension.TaskState.DONE) { + dialog.setMessage("Finishing Import...") + lifecycleScope.launchWhenStarted { + state.additionalData!!.forEach { + it.source = extension.id + viewModel.importWeight(it) + } + dialog.dismiss() } - dialog.dismiss() } } } } + + lifecycleScope.launch { + if (!extension.permissionsGranted() && request != null) { + request!!.launch(extension.requestInput) + } + } } } diff --git a/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt index d2a0eb6..8186e99 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsFragment.kt @@ -1,23 +1,19 @@ package com.dzeio.openhealth.ui.extensions -import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import com.dzeio.openhealth.R import com.dzeio.openhealth.adapters.ExtensionAdapter import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.databinding.FragmentExtensionsBinding import com.dzeio.openhealth.extensions.Extension -import com.dzeio.openhealth.extensions.GoogleFit -import com.dzeio.openhealth.utils.PermissionsManager import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch @AndroidEntryPoint class ExtensionsFragment : @@ -32,18 +28,6 @@ class ExtensionsFragment : 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?) { super.onViewCreated(view, savedInstanceState) @@ -52,62 +36,38 @@ class ExtensionsFragment : val manager = LinearLayoutManager(requireContext()) recycler.layoutManager = manager - val adapter = ExtensionAdapter() + val adapter = ExtensionAdapter(viewModel.config) adapter.onItemClick = { activeExtension = it + activeExtension.enable(this) Log.d(TAG, "${it.id}: ${it.name}") - this.extensionPermissionsVerified(it) + lifecycleScope.launch { + extensionIsConnected(it) + } } recycler.adapter = adapter - val list = arrayOf( - GoogleFit() - ).toList() - + val list = viewModel.extensions list.forEach { - it.init(requireActivity()) + it.enable(this) } adapter.set(list) } - @Deprecated("Deprecated in Java") - 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) { + private suspend fun extensionIsConnected(it: Extension) { // check if it is connected if (it.isConnected()) { gotoExtension(it) return } - // IDK: maybe give less liberty to the extension IDK val ld = it.connect() - ld.observe(viewLifecycleOwner) { state -> - Log.d(TAG, state.toString()) - if (state == Extension.States.DONE) { - gotoExtension(it) - } + if (ld) { + gotoExtension(it) } + // handle if extension can't be connected } private fun gotoExtension(it: Extension) { diff --git a/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsViewModel.kt index ec14b23..646e69e 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/extensions/ExtensionsViewModel.kt @@ -1,31 +1,16 @@ package com.dzeio.openhealth.ui.extensions -import androidx.lifecycle.MutableLiveData import com.dzeio.openhealth.core.BaseViewModel -import com.dzeio.openhealth.data.weight.Weight -import com.dzeio.openhealth.data.weight.WeightRepository +import com.dzeio.openhealth.extensions.ExtensionFactory +import com.dzeio.openhealth.utils.Configuration import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject - @HiltViewModel class ExtensionsViewModel @Inject internal constructor( - private val weightRepository: WeightRepository + val config: Configuration ) : BaseViewModel() { - val text = MutableLiveData().apply { - value = "This is slideshow Fragment" - } - val importProgress = MutableLiveData().apply { - value = 0 - } - // If -1 progress is undetermined - // If 0 no progress bar - // Else progress bar - val importProgressTotal = MutableLiveData().apply { - value = 0 - } + val extensions = ExtensionFactory.getAll() - suspend fun importWeight(weight: Weight) = weightRepository.addWeight(weight) - suspend fun deleteFromSource(source: String) = weightRepository.deleteFromSource(source) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt index 9675272..0e10999 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/home/HomeFragment.kt @@ -15,7 +15,7 @@ import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.databinding.FragmentHomeBinding import com.dzeio.openhealth.graphs.WeightChart import com.dzeio.openhealth.ui.weight.WeightDialog -import com.dzeio.openhealth.units.UnitFactory +import com.dzeio.openhealth.units.Units import com.dzeio.openhealth.utils.DrawUtils import com.dzeio.openhealth.utils.GraphUtils import com.google.android.material.color.MaterialColors @@ -56,7 +56,7 @@ class HomeFragment : BaseFragment(HomeViewMo } } val waterUnit = - UnitFactory.volume(settings.getString("water_unit", "milliliter") ?: "Milliliter") + Units.Volume.find(settings.getString("water_unit", "milliliter") ?: "Milliliter") binding.fragmentHomeWaterTotal.text = String.format( @@ -147,7 +147,7 @@ class HomeFragment : BaseFragment(HomeViewMo private fun updateWater(newValue: Int) { val waterUnit = - UnitFactory.volume(settings.getString("water_unit", "milliliter") ?: "Milliliter") + Units.Volume.find(settings.getString("water_unit", "milliliter") ?: "Milliliter") binding.fragmentHomeWaterCurrent.text = String.format( diff --git a/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt index 3559612..19fcbe2 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt @@ -14,8 +14,10 @@ import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding import com.google.android.material.color.MaterialColors import dagger.hilt.android.AndroidEntryPoint import java.text.DateFormat +import java.util.Calendar import java.util.Date import java.util.Locale +import java.util.TimeZone @AndroidEntryPoint class StepsHomeFragment : @@ -104,6 +106,17 @@ class StepsHomeFragment : viewModel.items.observe(viewLifecycleOwner) { 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.refreshRate = 60 // chart.animation.duration = 300 diff --git a/app/src/main/java/com/dzeio/openhealth/ui/weight/ListWeightFragment.kt b/app/src/main/java/com/dzeio/openhealth/ui/weight/ListWeightFragment.kt index aa0d021..5c0ed3b 100644 --- a/app/src/main/java/com/dzeio/openhealth/ui/weight/ListWeightFragment.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/weight/ListWeightFragment.kt @@ -113,10 +113,13 @@ class ListWeightFragment : return when (item.itemId) { R.id.action_add -> { findNavController().navigate( - ListWeightFragmentDirections.actionNavListWeightToNavAddWeightDialog() + ListWeightFragmentDirections.actionNavListWeightToNavWeightDialog( + WeightDialog.DialogTypes.ADD_WEIGHT.ordinal + ) ) true } + else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/res/layout/activity_privacy_policy.xml b/app/src/main/res/layout/activity_privacy_policy.xml new file mode 100644 index 0000000..a624c0e --- /dev/null +++ b/app/src/main/res/layout/activity_privacy_policy.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/menu/bottom_menu.xml b/app/src/main/res/menu/bottom_menu.xml index 788f5aa..418cd82 100644 --- a/app/src/main/res/menu/bottom_menu.xml +++ b/app/src/main/res/menu/bottom_menu.xml @@ -18,6 +18,7 @@ diff --git a/app/src/main/res/values/health_permissions.xml b/app/src/main/res/values/health_permissions.xml new file mode 100644 index 0000000..2c2a0fe --- /dev/null +++ b/app/src/main/res/values/health_permissions.xml @@ -0,0 +1,9 @@ + + + + androidx.health.permission.HeartRate.READ + androidx.health.permission.HeartRate.WRITE + androidx.health.permission.Steps.READ + androidx.health.permission.Steps.WRITE + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 089ad66..04a11bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,11 +13,11 @@ Kilogram Kilograms - %1$skg + kg Pound Pounds - %1$slbs + lbs Milliliter Milliliters diff --git a/build.gradle.kts b/build.gradle.kts index b1d1846..75f7d50 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ buildscript { classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.5") // 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 classpath("com.google.android.gms:oss-licenses-plugin:0.10.5") @@ -12,9 +12,9 @@ buildscript { } plugins { - id("com.android.application") version "7.2.2" apply false - id("com.android.library") version "7.2.2" apply false - id("org.jetbrains.kotlin.android") version "1.6.10" apply false + id("com.android.application") version "7.3.1" apply false + id("com.android.library") version "7.3.1" apply false + id("org.jetbrains.kotlin.android") version "1.6.21" apply false } task("clean") {