diff --git a/app/build.gradle b/app/build.gradle index 83ae9ed..6821d9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,11 +101,14 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'javax.inject:javax.inject:1' - implementation 'com.google.android.material:material:1.7.0-alpha02' + implementation 'com.google.android.material:material:1.7.0-alpha03' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0' + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3" + // Settings implementation "androidx.preference:preference-ktx:1.2.0" @@ -116,6 +119,11 @@ dependencies { implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0' implementation 'androidx.navigation:navigation-ui-ktx:2.5.0' + // Paging + implementation "androidx.paging:paging-runtime:3.1.1" + implementation "androidx.paging:paging-runtime-ktx:3.1.1" + + // Services implementation 'androidx.work:work-runtime-ktx:2.7.1' @@ -128,8 +136,8 @@ dependencies { implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' // Hilt - implementation "com.google.dagger:hilt-android:2.40.5" - kapt "com.google.dagger:hilt-compiler:2.40.5" + implementation 'com.google.dagger:hilt-android:2.42' + kapt 'com.google.dagger:hilt-compiler:2.42' // Google Fit implementation "com.google.android.gms:play-services-fitness:21.1.0" @@ -137,11 +145,16 @@ dependencies { // Samsung Health implementation files('libs/samsung-health-data-1.5.0.aar') - implementation "com.google.code.gson:gson:2.8.9" + implementation 'com.google.code.gson:gson:2.9.0' // ROOM implementation "androidx.room:room-runtime:2.4.2" kapt "androidx.room:room-compiler:2.4.2" implementation "androidx.room:room-ktx:2.4.2" testImplementation "androidx.room:room-testing:2.4.2" + + // Futures + implementation 'com.google.guava:guava:31.1-jre' + implementation "androidx.concurrent:concurrent-futures:1.1.0" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 600b804..233d020 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,7 +34,7 @@ android:value="@integer/google_play_services_version" /> @@ -43,6 +43,11 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/Application.kt b/app/src/main/java/com/dzeio/openhealth/Application.kt index f5ad855..cd5fc34 100644 --- a/app/src/main/java/com/dzeio/openhealth/Application.kt +++ b/app/src/main/java/com/dzeio/openhealth/Application.kt @@ -1,9 +1,7 @@ package com.dzeio.openhealth import android.app.Application -import android.content.Context import android.content.SharedPreferences -import android.content.res.Resources import androidx.preference.PreferenceManager import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp @@ -27,9 +25,15 @@ class Application : Application() { val locale = Locale(lang) Locale.setDefault(locale) - val overrideConfiguration = baseContext.resources.configuration - overrideConfiguration.locale = locale - val context: Context = createConfigurationContext(overrideConfiguration) - val resources: Resources = context.getResources() +// val overrideConfiguration = baseContext.resources.configuration +// overrideConfiguration.locale = locale +// val context: Context = createConfigurationContext(overrideConfiguration) +// val resources: Resources = context.resources + } + + /** + * SharedPreferences Key + */ + object Settings { } } diff --git a/app/src/main/java/com/dzeio/openhealth/adapters/StepsAdapter.kt b/app/src/main/java/com/dzeio/openhealth/adapters/StepsAdapter.kt new file mode 100644 index 0000000..c6f7fb6 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/adapters/StepsAdapter.kt @@ -0,0 +1,28 @@ +package com.dzeio.openhealth.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.dzeio.openhealth.core.BaseAdapter +import com.dzeio.openhealth.core.BaseViewHolder +import com.dzeio.openhealth.data.step.Step +import com.dzeio.openhealth.databinding.LayoutItemListBinding + +class StepsAdapter() : BaseAdapter() { + + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutItemListBinding + get() = LayoutItemListBinding::inflate + + var onItemClick: ((weight: Step) -> Unit)? = null + + override fun onBindData( + holder: BaseViewHolder, + item: Step, + position: Int + ) { + holder.binding.value.text = "${item.value}ml" + holder.binding.datetime.text = item.formatTimestamp() + holder.binding.edit.setOnClickListener { + onItemClick?.invoke(item) + } + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/core/BaseAdapter.kt b/app/src/main/java/com/dzeio/openhealth/core/BaseAdapter.kt index eaa9481..1e5e2f8 100644 --- a/app/src/main/java/com/dzeio/openhealth/core/BaseAdapter.kt +++ b/app/src/main/java/com/dzeio/openhealth/core/BaseAdapter.kt @@ -32,7 +32,7 @@ abstract class BaseAdapter : RecyclerView.Adapter, item: T, position: Int) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { - return BaseViewHolder( + return BaseViewHolder( bindingInflater(LayoutInflater.from(parent.context), parent, false) ) } diff --git a/app/src/main/java/com/dzeio/openhealth/core/BaseService.kt b/app/src/main/java/com/dzeio/openhealth/core/BaseService.kt index 7af8e40..17bfff7 100644 --- a/app/src/main/java/com/dzeio/openhealth/core/BaseService.kt +++ b/app/src/main/java/com/dzeio/openhealth/core/BaseService.kt @@ -2,24 +2,20 @@ package com.dzeio.openhealth.core import android.content.Context import android.util.Log +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager -import androidx.work.WorkRequest import androidx.work.Worker import androidx.work.WorkerParameters abstract class BaseService(context: Context, params: WorkerParameters) : Worker(context, params) { - companion object { - fun schedule(tag: String, request: WorkRequest, context: Context) { - WorkManager.getInstance(context) - .cancelAllWorkByTag(tag) - + fun schedule(tag: String, request: PeriodicWorkRequest, context: Context) { Log.d("OpenHealth/BaseService", "Scheduled Job $tag") WorkManager.getInstance(context) - .enqueue(request) - + .enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.KEEP, request) } } } \ No newline at end of file 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 4850ce6..96fb723 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/AppDatabase.kt @@ -4,6 +4,8 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import com.dzeio.openhealth.data.step.Step +import com.dzeio.openhealth.data.step.StepDao import com.dzeio.openhealth.data.water.Water import com.dzeio.openhealth.data.water.WaterDao import com.dzeio.openhealth.data.weight.Weight @@ -12,7 +14,8 @@ import com.dzeio.openhealth.data.weight.WeightDao @Database( entities = [ Weight::class, - Water::class + Water::class, + Step::class ], version = 1, exportSchema = false @@ -23,6 +26,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun weightDao(): WeightDao abstract fun waterDao(): WaterDao + abstract fun stepDao(): StepDao companion object { private const val DATABASE_NAME = "open_health" diff --git a/app/src/main/java/com/dzeio/openhealth/data/step/Step.kt b/app/src/main/java/com/dzeio/openhealth/data/step/Step.kt new file mode 100644 index 0000000..6a6b361 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/data/step/Step.kt @@ -0,0 +1,45 @@ +package com.dzeio.openhealth.data.step + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.sql.Date +import java.text.DateFormat +import java.util.Calendar +import java.util.TimeZone + +@Entity() +data class Step( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + var value: Float = 0f, + @ColumnInfo(index = true) + /** + * Timestamp down to an hour + * + * ex: 2022-09-22 10:00:00 + */ + var timestamp: Long = 0, + var source: String = "OpenHealth" +) { + + init { + if (timestamp == 0L) { + val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + + timestamp = cal.timeInMillis + } + } + + fun formatTimestamp(): String = DateFormat.getDateInstance().format(Date(timestamp)) + + fun isToday(): Boolean { + val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + cal.set(Calendar.MILLISECOND, 0) + return timestamp == cal.timeInMillis + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/data/step/StepDao.kt b/app/src/main/java/com/dzeio/openhealth/data/step/StepDao.kt new file mode 100644 index 0000000..5d97152 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/data/step/StepDao.kt @@ -0,0 +1,35 @@ +package com.dzeio.openhealth.data.step + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.dzeio.openhealth.core.BaseDao +import kotlinx.coroutines.flow.Flow + +@Dao +interface StepDao : BaseDao { + + @Query("SELECT * FROM Step ORDER BY timestamp DESC") + fun getAll(): Flow> + + @Query("SELECT * FROM Step where id = :weightId") + fun getOne(weightId: Long): Flow + + @Query("Select count(*) from Step") + fun getCount(): Flow + + @Query("Select * FROM Step ORDER BY timestamp DESC LIMIT 1") + fun last(): Flow + + @Query("DELETE FROM Step where source = :source") + suspend fun deleteFromSource(source: String) + + @Update + fun updateNF(vararg obj: Step) + @Insert + fun insertNF(vararg obj: Step) + + @Query("Select * FROM Step ORDER BY timestamp DESC LIMIT 1") + fun lastNF(): Step? +} diff --git a/app/src/main/java/com/dzeio/openhealth/data/step/StepRepository.kt b/app/src/main/java/com/dzeio/openhealth/data/step/StepRepository.kt new file mode 100644 index 0000000..ad6774a --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/data/step/StepRepository.kt @@ -0,0 +1,26 @@ +package com.dzeio.openhealth.data.step + +import kotlinx.coroutines.flow.filter +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StepRepository @Inject constructor( + private val stepDao: StepDao +) { + fun getSteps() = stepDao.getAll() + + fun lastStep() = stepDao.last() + + fun getStep(id: Long) = stepDao.getOne(id) + + suspend fun addStep(value: Step) = stepDao.insert(value) + suspend fun updateStep(value: Step) = stepDao.update(value) + + suspend fun deleteStep(value: Step) = stepDao.delete(value) + suspend fun deleteFromSource(value: String) = stepDao.deleteFromSource(value) + + fun todayStep() = lastStep().filter { + return@filter it != null && it.isToday() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/data/step/StepSource.kt b/app/src/main/java/com/dzeio/openhealth/data/step/StepSource.kt new file mode 100644 index 0000000..79a8e6c --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/data/step/StepSource.kt @@ -0,0 +1,90 @@ +package com.dzeio.openhealth.data.step + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.SystemClock +import android.util.Log +import androidx.preference.PreferenceManager +import com.dzeio.openhealth.Application +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking + +class StepSource( + private val context: Context, + private val callback: ((Float) -> Unit)? = null +): SensorEventListener { + + companion object { + const val TAG = "${Application.TAG}/StepSource" + } + + private val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + private var timeSinceLastRecord: Long + get() { + return prefs.getLong("steps_time_since_last_record", Long.MAX_VALUE) + } + set(value) { + val editor = prefs.edit() + editor.putLong("steps_time_since_last_record", value) + editor.commit() + } + private var stepsAsOfLastRecord: Float + get() { + return prefs.getFloat("steps_as_of_last_record", 0f) + } + set(value) { + val editor = prefs.edit() + editor.putFloat("steps_as_of_last_record", value) + editor.commit() + } + + + init { + Log.d(TAG, "Setting up") + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + val stepCountSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) + stepCountSensor.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL, SensorManager.SENSOR_DELAY_NORMAL) + Log.d(TAG, "should be setup :D") + } + } + + val events = Channel(10) + + override fun onSensorChanged(ev: SensorEvent?) { + if (ev == null) { + return + } + // Log.d(TAG, "Sensor changed: $ev") + ev.values.firstOrNull()?.let { + val timeSinceLastBoot = System.currentTimeMillis() - SystemClock.elapsedRealtime() + + val diff = it - stepsAsOfLastRecord + stepsAsOfLastRecord = it + + // don't send changes since it wasn't made when the app was running + if (timeSinceLastBoot < timeSinceLastRecord) { + Log.d(TAG, "Skipping since we don't know when many steps are taken since last boot ($timeSinceLastRecord, $timeSinceLastBoot)") + timeSinceLastRecord = timeSinceLastBoot + return@let + } + + timeSinceLastRecord = timeSinceLastBoot + + runBlocking { + events.send(diff) + } + callback?.invoke(diff) + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + Log.d(TAG, "[Accuracy changed]: Sensor: $sensor, Accuracy: $accuracy") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt index 22df54b..9423bc9 100644 --- a/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt +++ b/app/src/main/java/com/dzeio/openhealth/data/weight/WeightDao.kt @@ -1,8 +1,8 @@ package com.dzeio.openhealth.data.weight -import androidx.room.* +import androidx.room.Dao +import androidx.room.Query import com.dzeio.openhealth.core.BaseDao -import dagger.Provides import kotlinx.coroutines.flow.Flow @Dao @@ -11,10 +11,9 @@ interface WeightDao : BaseDao { @Query("SELECT * FROM Weight ORDER BY timestamp") fun getAll(): Flow> - @Query("SELECT * FROM Weight where id = :weightId") + @Query("SELECT * FROM Weight WHERE id = :weightId") fun getOne(weightId: Long): Flow - @Query("Select count(*) from Weight") fun getCount(): Flow diff --git a/app/src/main/java/com/dzeio/openhealth/di/DatabaseModule.kt b/app/src/main/java/com/dzeio/openhealth/di/DatabaseModule.kt index 15ad53a..10841ac 100644 --- a/app/src/main/java/com/dzeio/openhealth/di/DatabaseModule.kt +++ b/app/src/main/java/com/dzeio/openhealth/di/DatabaseModule.kt @@ -2,6 +2,7 @@ package com.dzeio.openhealth.di import android.content.Context import com.dzeio.openhealth.data.AppDatabase +import com.dzeio.openhealth.data.step.StepDao import com.dzeio.openhealth.data.water.WaterDao import com.dzeio.openhealth.data.weight.WeightDao import dagger.Module @@ -30,4 +31,9 @@ class DatabaseModule { fun provideWaterDao(appDatabase: AppDatabase): WaterDao { return appDatabase.waterDao() } + + @Provides + fun provideStepsDao(appDatabase: AppDatabase): StepDao { + return appDatabase.stepDao() + } } \ No newline at end of file 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 6e0ce44..ccce665 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/Extension.kt @@ -9,9 +9,9 @@ import com.dzeio.openhealth.data.weight.Weight /** * Extension Schema * - * Version: 1.0.0 + * Version: 0.1.0 */ -abstract class Extension { +interface Extension { data class ImportState( val state: States = States.WIP, @@ -33,6 +33,8 @@ abstract class Extension { STEPS /** + * Google Fit: + * * STEP_COUNT_CUMULATIVE * ACTIVITY_SEGMENT * SLEEP_SEGMENT @@ -44,47 +46,52 @@ abstract class Extension { */ } + + val permissions: Array? + + val permissionsText: String? + /** * the Source ID * * DO NOT CHANGE IT AFTER THE EXTENSION IS IN PRODUCTION */ - abstract val id: String + val id: String /** * The Extension Display Name */ - abstract val name: String + val name: String /** * Initialize hte Extension * * It is run Before any functions is launched and after events handlers are set */ - abstract fun init(activity: Activity): Array + fun init(activity: Activity): Array /** * A status shown on the extension list page */ - open fun getStatus(): String { + fun getStatus(): String { return "No Status set..." } /** * Function that will check */ - abstract fun isAvailable(): Boolean + fun isAvailable(): Boolean /** - * idk + * return if the extension is already connected to remote of not */ - abstract fun isConnected(): Boolean + fun isConnected(): Boolean - open fun connect(): LiveData { + fun connect(): LiveData { return MutableLiveData(States.DONE) } - open fun importWeight(): LiveData> { + fun importWeight(): LiveData> { return MutableLiveData(ImportState(States.DONE)) } @@ -92,7 +99,7 @@ abstract class Extension { * function run when outgoing sync is enabled and new value is added * or manual export is launched */ - open fun exportWeight(weight: Weight): LiveData { + fun exportWeight(weight: Weight): LiveData { return MutableLiveData(States.DONE) } @@ -100,20 +107,8 @@ abstract class Extension { * Activity Events */ - /** - * Same as Activity/Fragment onRequestPermissionResult - * - * But it will only be launched if grantResults[0] == PackageManager.PERMISSION_GRANTED - */ - open fun onRequestPermissionResult( - requestCode: Int, - permission: Array, - grantResult: IntArray - ) { - } - /** * Same as Activity/Fragment onActivityResult */ - open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} } diff --git a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt index 3df4c13..9585045 100644 --- a/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt +++ b/app/src/main/java/com/dzeio/openhealth/extensions/GoogleFit.kt @@ -3,10 +3,7 @@ package com.dzeio.openhealth.extensions import android.Manifest import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build import android.util.Log -import androidx.core.app.ActivityCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.dzeio.openhealth.data.weight.Weight @@ -21,7 +18,7 @@ import java.util.Date import java.util.TimeZone import java.util.concurrent.TimeUnit -class GoogleFit() : Extension() { +class GoogleFit: Extension { companion object { const val TAG = "GoogleFitConnector" } @@ -31,10 +28,16 @@ class GoogleFit() : Extension() { override val id = "GoogleFit" override val name = "Google Fit" - override fun init(activity: Activity): Array { + override val permissions = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION + ) + + override val permissionsText: String = "Please" + + override fun init(activity: Activity): Array { this.activity = activity return arrayOf( - Data.WEIGHT + Extension.Data.WEIGHT ) } @@ -55,47 +58,14 @@ class GoogleFit() : Extension() { // .addDataType(DataType.TYPE_CALORIES_EXPENDED) .build() -// private fun checkPermissionsAndRun(data: Data) { -// if (permissionApproved()) { -// signIn(data) -// } else { -// Log.d(TAG, "Asking for permission") -// // Ask for permission -// ActivityCompat.requestPermissions( -// activity, -// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), -// data.ordinal -// ) -// } -// } + private val connectLiveData: MutableLiveData = MutableLiveData(Extension.States.WIP) - private fun permissionApproved(): Boolean = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission( - activity, - Manifest.permission.ACCESS_FINE_LOCATION - ) - } else { - true - } - - private val connectLiveData: MutableLiveData = MutableLiveData(States.WIP) - - override fun connect(): LiveData { - - if (!permissionApproved()) { - ActivityCompat.requestPermissions( - activity, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - 87531 - ) - return connectLiveData - } + override fun connect(): LiveData { if (isConnected()) { - connectLiveData.value = States.DONE + connectLiveData.value = Extension.States.DONE } else { - Log.d("GoogleFitImporter", "Signing In") + Log.d(this.name, "Signing In") GoogleSignIn.requestPermissions( activity, 124887, @@ -105,19 +75,6 @@ class GoogleFit() : Extension() { return connectLiveData } -// private fun signIn(data: Data) { -// if (isConnected()) { -// startImport(data) -// } else { -// Log.d("GoogleFitImporter", "Signing In") -// GoogleSignIn.requestPermissions( -// activity, -// data.ordinal, -// getGoogleAccount(), fitnessOptions -// ) -// } -// } - private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions) private val timeRange by lazy { @@ -131,7 +88,7 @@ class GoogleFit() : Extension() { return@lazy arrayOf(startTime, endTime) } - private fun startImport(data: Data) { + private fun startImport(data: Extension.Data) { Log.d("GoogleFitImporter", "Importing for ${data.name}") val dateFormat = DateFormat.getDateInstance() @@ -142,7 +99,7 @@ class GoogleFit() : Extension() { var timeUnit = TimeUnit.MILLISECONDS when (data) { - Data.STEPS -> { + Extension.Data.STEPS -> { type = DataType.TYPE_STEP_COUNT_CUMULATIVE } else -> {} @@ -157,7 +114,7 @@ class GoogleFit() : Extension() { ) } - private fun runRequest(request: DataReadRequest, data: Data) { + private fun runRequest(request: DataReadRequest, data: Extension.Data) { Fitness.getHistoryClient( activity, GoogleSignIn.getAccountForExtension(activity, fitnessOptions) @@ -191,7 +148,7 @@ class GoogleFit() : Extension() { for (field in dp.dataType.fields) { Log.i(TAG, "\tField: ${field.name} Value: ${dp.getValue(field)}") when (data) { - Data.WEIGHT -> { + Extension.Data.WEIGHT -> { val weight = Weight() weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS) weight.weight = dp.getValue(field).asFloat() @@ -200,17 +157,17 @@ class GoogleFit() : Extension() { list.add(weight) weightLiveData.value = - ImportState(States.WIP, list) + Extension.ImportState(Extension.States.WIP, list) } else -> {} } } } when (data) { - Data.WEIGHT -> { + Extension.Data.WEIGHT -> { weightLiveData.value = - ImportState( - States.DONE, + Extension.ImportState( + Extension.States.DONE, weightLiveData.value?.list ?: ArrayList() ) @@ -224,39 +181,27 @@ class GoogleFit() : Extension() { } } - /** - * Currently not usable - */ - override fun onRequestPermissionResult( - requestCode: Int, - permission: Array, - grantResult: IntArray - ) { - connect() - // signIn(Data.values()[requestCode]) - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + Log.d(this.name, "[$requestCode] -> [$resultCode]: $data") if (requestCode == 0) { return } - connectLiveData.value = States.DONE + + if (resultCode == Activity.RESULT_OK) connectLiveData.value = Extension.States.DONE // signIn(Data.values()[requestCode]) } - private lateinit var weightLiveData: MutableLiveData> + private lateinit var weightLiveData: MutableLiveData> - override fun importWeight(): LiveData> { + override fun importWeight(): LiveData> { weightLiveData = MutableLiveData( - ImportState( - States.WIP + Extension.ImportState( + Extension.States.WIP ) ) - startImport(Data.WEIGHT) - -// checkPermissionsAndRun(Data.WEIGHT) + startImport(Extension.Data.WEIGHT) return weightLiveData } diff --git a/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt b/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt index 9ee9bf3..1ed7fb1 100644 --- a/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt +++ b/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationChannels.kt @@ -1,10 +1,12 @@ package com.dzeio.openhealth.interfaces +import android.app.NotificationManager + enum class NotificationChannels( val id: String, val channelName: String, val importance: Int ) { - // 3 is IMPORTANCE_DEFAULT - WATER("water", "Water Notifications", 3) + WATER("water", "Water Notifications", NotificationManager.IMPORTANCE_DEFAULT), + SERVICE("service", "Open Health Service", NotificationManager.IMPORTANCE_MIN) } diff --git a/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationIds.kt b/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationIds.kt index 8f9af97..8668f81 100644 --- a/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationIds.kt +++ b/app/src/main/java/com/dzeio/openhealth/interfaces/NotificationIds.kt @@ -1,5 +1,10 @@ package com.dzeio.openhealth.interfaces enum class NotificationIds { - WaterIntake + WaterIntake, + + /** + * Open Health Main Service Notification ID + */ + Service } \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/services/OpenHealthService.kt b/app/src/main/java/com/dzeio/openhealth/services/OpenHealthService.kt new file mode 100644 index 0000000..8ec23d3 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/services/OpenHealthService.kt @@ -0,0 +1,132 @@ +package com.dzeio.openhealth.services + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import android.widget.Toast +import androidx.core.app.NotificationCompat +import androidx.navigation.NavDeepLinkBuilder +import com.dzeio.openhealth.Application +import com.dzeio.openhealth.R +import com.dzeio.openhealth.data.AppDatabase +import com.dzeio.openhealth.data.step.Step +import com.dzeio.openhealth.data.step.StepRepository +import com.dzeio.openhealth.data.step.StepRepository_Factory +import com.dzeio.openhealth.data.step.StepSource +import com.dzeio.openhealth.interfaces.NotificationChannels +import com.dzeio.openhealth.interfaces.NotificationIds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull + +class OpenHealthService : Service() { + + companion object { + private const val TAG = "${Application.TAG}/OpenHealthService" + } + + val stepRepository: StepRepository + get() = StepRepository_Factory.newInstance(AppDatabase.getInstance(applicationContext).stepDao()) + + private var mNM: NotificationManager? = null + + // Unique Identification Number for the Notification. + // We use it on Notification start, and to cancel it. + private val NOTIFICATION: Int = 8942 + + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + job) + + override fun onCreate() { + mNM = getSystemService(NOTIFICATION_SERVICE) as NotificationManager? + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + Log.i("LocalService", "Received start id $startId: $intent") + + scope.launch { + val source = StepSource(this@OpenHealthService) + source.events.receiveAsFlow().collectLatest { + Log.d(TAG, "Received value: $it") + + if (it <= 0f) { + Log.d(TAG, "No new steps registered ($it)") + return@collectLatest + } + Log.d(TAG, "New steps registered: $it") + val step = withTimeoutOrNull(100) { + return@withTimeoutOrNull stepRepository.todayStep().firstOrNull() + } + Log.d(TAG, "stepRepository: $step") + if (step != null) { + step.value += it + stepRepository.updateStep(step) + } else { + stepRepository.addStep(Step(value = it)) + } + Log.d(TAG, "Added step!") + } + } + + + // Display a notification about us starting. We put an icon in the status bar. + + startForeground(NotificationIds.Service.ordinal, showNotification()) + + return START_STICKY + } + + override fun onDestroy() { + stopForeground(true) + + + + // Tell the user we stopped. + Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + /** + * Show a notification while this service is running. + */ + private fun showNotification(): Notification { + // The PendingIntent to launch our activity if the user selects this notification + val flag = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0 + val intent = NavDeepLinkBuilder(this) + .setGraph(R.navigation.mobile_navigation) + .setDestination(R.id.nav_home) + // Will nav to water home when there will be a way to add it there +// .setDestination(R.id.nav_water_home) + .createTaskStackBuilder() + .getPendingIntent(0, flag) + + // Set the info for the views that show in the notification panel. + val notification: Notification = NotificationCompat.Builder(this, NotificationChannels.SERVICE.id) + .setSmallIcon(R.drawable.ic_logo_small) +// .setTicker("Pouet") // the status text + .setWhen(System.currentTimeMillis()) // the time stamp + .setContentTitle("Open Health Service") // the label of the entry + .setContentText("Watching for your steps") // the contents of the entry + .setContentIntent(intent) // The intent to send when the entry is clicked + .build() + + // Send the notification. + mNM!!.notify(NotificationIds.Service.ordinal, notification) + + return notification + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/services/StepCountService.kt b/app/src/main/java/com/dzeio/openhealth/services/StepCountService.kt deleted file mode 100644 index 6c3c5a5..0000000 --- a/app/src/main/java/com/dzeio/openhealth/services/StepCountService.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.dzeio.openhealth.services - -import android.annotation.SuppressLint -import android.app.job.JobParameters -import android.app.job.JobService -import android.content.Context -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import android.util.Log - -@SuppressLint("SpecifyJobSchedulerIdRange") -class StepCountService : JobService(), SensorEventListener { - override fun onStartJob(params: JobParameters?): Boolean { - Log.d("StepCountService", "Service Started") - val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager - val stepCountSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) - stepCountSensor.let { - sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL) - } - return true - } - - override fun onStopJob(params: JobParameters?): Boolean { - Log.d("StepCountService", "Service Stopped :(") - return true - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { - Log.d("StepCountService", "Accuracy changed $sensor, $accuracy") - } - - override fun onSensorChanged(event: SensorEvent?) { - event?.let { - Log.d("StepCountService", "Event Triggered: $it") - it.values.firstOrNull()?.let { value -> - Log.d("StepCountService", "Step Count $value") - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/MainActivity.kt b/app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt similarity index 77% rename from app/src/main/java/com/dzeio/openhealth/MainActivity.kt rename to app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt index e65f806..87e43f2 100644 --- a/app/src/main/java/com/dzeio/openhealth/MainActivity.kt +++ b/app/src/main/java/com/dzeio/openhealth/ui/MainActivity.kt @@ -1,5 +1,6 @@ -package com.dzeio.openhealth +package com.dzeio.openhealth.ui +import android.app.ActivityManager import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context @@ -17,16 +18,22 @@ import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController -import androidx.work.WorkManager +import com.dzeio.openhealth.Application +import com.dzeio.openhealth.R import com.dzeio.openhealth.core.BaseActivity import com.dzeio.openhealth.databinding.ActivityMainBinding import com.dzeio.openhealth.interfaces.NotificationChannels -import com.dzeio.openhealth.services.WaterReminderService +import com.dzeio.openhealth.services.OpenHealthService +import com.dzeio.openhealth.workers.WaterReminderService import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : BaseActivity() { + companion object { + const val TAG = "${Application.TAG}/MainActivity" + } + private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var navController: NavController @@ -57,21 +64,30 @@ class MainActivity : BaseActivity() { binding.bottomNav.setupWithNavController(navController) -// binding.bottomNav.setOnItemSelectedListener { -// val currentFragment = supportFragmentManager.fragments.last() -// // currentFragment.javaClass.canonicalName +// registerForActivityResult(ActivityResultContracts.RequestPermission()) { // -// navController. -// -// false // } +// .launch(Manifest.permission.ACTIVITY_RECOGNITION) createNotificationChannel() // Services - WorkManager.getInstance(this) - .cancelAllWork() WaterReminderService.setup(this) +// StepCountService.setup(this) + + this.betterStartService(OpenHealthService::class.java) + } + + private fun betterStartService(service: Class) { + val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (runninService in activityManager.getRunningServices(Integer.MAX_VALUE)) { + if (service.name.equals(runninService.service.className)) { + Log.w(TAG, "Service already existing, not starting again") + return + } + } + Log.i(TAG, "Starting service ${service.name}") + Intent(this, service).also { intent -> startService(intent) } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -87,18 +103,6 @@ class MainActivity : BaseActivity() { NavigationUI.onNavDestinationSelected(item, navController) || super.onOptionsItemSelected(item) - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - Log.d("MainActivity", "Result $requestCode") - for (fragment in supportFragmentManager.primaryNavigationFragment!!.childFragmentManager.fragments) { - fragment.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - } - @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) 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 e1e29d9..932f42b 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 @@ -32,5 +32,9 @@ class BrowseFragment : binding.waterIntake.setOnClickListener { findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome()) } + + binding.steps.setOnClickListener { + findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToStepsHomeFragment()) + } } } 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 cdb3831..72386ca 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,22 +1,22 @@ package com.dzeio.openhealth.ui.extensions -import android.app.ProgressDialog import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.RequiresApi +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts 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 @AndroidEntryPoint @@ -32,6 +32,16 @@ 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) @@ -43,20 +53,9 @@ class ExtensionsFragment : val adapter = ExtensionAdapter() adapter.onItemClick = { activeExtension = it - Log.d(it.id, it.name) - if (it.isConnected()) { - Log.d(it.id, "Continue!") - findNavController().navigate( - ExtensionsFragmentDirections.actionNavExtensionsToNavExtension( - it.id - ) - ) - } else { - val ls = it.connect() - ls.observe(viewLifecycleOwner) { st -> - Log.d("States", st.name) - } - } + Log.d(TAG, "${it.id}: ${it.name}") + + this.extensionPermissionsVerified(it) } recycler.adapter = adapter @@ -71,56 +70,43 @@ class ExtensionsFragment : adapter.set(list) } + @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { activeExtension.onActivityResult(requestCode, resultCode, data) } - @RequiresApi(Build.VERSION_CODES.O) - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, - grantResults: IntArray - ) { - when { - grantResults.isEmpty() -> { - // If user interaction was interrupted, the permission request - // is cancelled and you receive empty arrays. - Log.i(TAG, "User interaction was cancelled.") - } + 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 - grantResults[0] == PackageManager.PERMISSION_GRANTED -> { - Log.d(TAG, "Granted") - activeExtension.onRequestPermissionResult(requestCode, permissions, grantResults) - } - else -> { - // Permission denied. + // show permissions + activityResult.launch(it.permissions) + return + } + extensionIsConnected(it) + } - // In this Activity we've chosen to notify the user that they - // have rejected a core permission for the app since it makes the Activity useless. - // We're communicating this message in a Snackbar since this is a sample app, but - // core permissions would typically be best requested during a welcome-screen flow. + private fun extensionIsConnected(it: Extension) { + // check if it is connected + if (it.isConnected()) { + gotoExtension(it) + return + } - // Additionally, it is important to remember that a permission might have been - // rejected without asking the user for permission (device policy or "Never ask - // again" prompts). Therefore, a user interface affordance is typically implemented - // when permissions are denied. Otherwise, your app could appear unresponsive to - // touches or interactions which have required permissions. - Log.e(TAG, "Error") -// Snackbar.make( -// findViewById(R.id.main_activity_view), -// R.string.permission_denied_explanation, -// Snackbar.LENGTH_INDEFINITE) -// .setAction(R.string.settings) { -// // Build intent that displays the App settings screen. -// val intent = Intent() -// intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS -// val uri = Uri.fromParts("package", -// BuildConfig.APPLICATION_ID, null) -// intent.data = uri -// intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK -// startActivity(intent) -// } -// .show() + // 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) } } } + + private fun gotoExtension(it: Extension) { + findNavController().navigate( + ExtensionsFragmentDirections.actionNavExtensionsToNavExtension(it.id) + ) + } } \ 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 8f9df22..1e33b0c 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 @@ -6,7 +6,6 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.RectF import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -24,9 +23,9 @@ import com.dzeio.openhealth.utils.DrawUtils import com.dzeio.openhealth.utils.GraphUtils import com.google.android.material.color.MaterialColors import dagger.hilt.android.AndroidEntryPoint -import kotlin.math.min import kotlinx.coroutines.flow.collectLatest import kotlin.math.max +import kotlin.math.min @AndroidEntryPoint class HomeFragment : BaseFragment(HomeViewModel::class.java) { @@ -204,7 +203,7 @@ class HomeFragment : BaseFragment(HomeViewMo animator.addUpdateListener { this.oldValue = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat() - Log.d("Test2", "${this.oldValue}") +// Log.d("Test2", "${this.oldValue}") DrawUtils.drawArc( canvas, 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 new file mode 100644 index 0000000..b4a0734 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeFragment.kt @@ -0,0 +1,79 @@ +package com.dzeio.openhealth.ui.steps + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import com.dzeio.openhealth.adapters.StepsAdapter +import com.dzeio.openhealth.core.BaseFragment +import com.dzeio.openhealth.databinding.FragmentStepsHomeBinding +import com.dzeio.openhealth.ui.water.StepsHomeViewModel +import com.dzeio.openhealth.utils.GraphUtils +import com.github.mikephil.charting.data.BarData +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.BarEntry +import com.google.android.material.color.MaterialColors +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class StepsHomeFragment : + BaseFragment(StepsHomeViewModel::class.java) { + + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentStepsHomeBinding = + FragmentStepsHomeBinding::inflate + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.init() + + val recycler = binding.list + + val manager = LinearLayoutManager(requireContext()) + recycler.layoutManager = manager + + val adapter = StepsAdapter() + adapter.onItemClick = { +// findNavController().navigate( +// WaterHomeFragmentDirections.actionNavWaterHomeToNavWaterEdit( +// it.id +// ) +// ) + } + recycler.adapter = adapter + + val chart = binding.chart + + GraphUtils.barChartSetup( + chart, + MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorPrimary + ), + MaterialColors.getColor( + requireView(), + com.google.android.material.R.attr.colorOnBackground + ) + ) + + chart.xAxis.valueFormatter = GraphUtils.DateValueFormatter(1000 * 60 * 60) + + viewModel.items.observe(viewLifecycleOwner) { list -> + adapter.set(list) + + val dataset = BarDataSet( + list.map { + return@map BarEntry( + (it.timestamp / 60 / 60 / 24).toFloat(), + it.value + ) + }, + "" + ) + + chart.data = BarData(dataset) + chart.invalidate() + } + } +} diff --git a/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeViewModel.kt b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeViewModel.kt new file mode 100644 index 0000000..276a4c2 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/ui/steps/StepsHomeViewModel.kt @@ -0,0 +1,26 @@ +package com.dzeio.openhealth.ui.water + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.dzeio.openhealth.core.BaseViewModel +import com.dzeio.openhealth.data.step.Step +import com.dzeio.openhealth.data.step.StepRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class StepsHomeViewModel@Inject internal constructor( + private val stepRepository: StepRepository +) : BaseViewModel() { + val items: MutableLiveData> = MutableLiveData() + + fun init() { + viewModelScope.launch { + stepRepository.getSteps().collectLatest { + items.postValue(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/utils/PermissionsManager.kt b/app/src/main/java/com/dzeio/openhealth/utils/PermissionsManager.kt new file mode 100644 index 0000000..0014a1e --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/utils/PermissionsManager.kt @@ -0,0 +1,20 @@ +package com.dzeio.openhealth.utils + +import android.content.Context +import android.content.pm.PackageManager + +object PermissionsManager { + + fun hasPermission(context: Context, permission: String): Boolean = context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED + + fun hasPermission(context: Context, permissions: Array): Boolean { + for (permission in permissions) { + val res = hasPermission(context, permission) + if (!res) { + return false + } + } + return true + } +} + diff --git a/app/src/main/java/com/dzeio/openhealth/workers/StepCountService.kt b/app/src/main/java/com/dzeio/openhealth/workers/StepCountService.kt new file mode 100644 index 0000000..b529d28 --- /dev/null +++ b/app/src/main/java/com/dzeio/openhealth/workers/StepCountService.kt @@ -0,0 +1,67 @@ +package com.dzeio.openhealth.workers + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkerParameters +import com.dzeio.openhealth.Application +import com.dzeio.openhealth.core.BaseService +import com.dzeio.openhealth.data.AppDatabase +import com.dzeio.openhealth.data.step.Step +import com.dzeio.openhealth.data.step.StepRepository +import com.dzeio.openhealth.data.step.StepSource +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull +import java.util.concurrent.TimeUnit + +@SuppressLint("SpecifyJobSchedulerIdRange") +class StepCountService( + private val context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + + + companion object { + const val TAG = "${Application.TAG}/StepCountService" + + fun setup(context: Context) { + BaseService.schedule( + TAG, + PeriodicWorkRequestBuilder(PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS) + .addTag(TAG) + .build(), + context + ) + } + } + + override suspend fun doWork(): Result { + Log.d(TAG, "Service Started") + val appDatabase = AppDatabase.getInstance(this.context) + val repo = StepRepository(appDatabase.stepDao()) + + val source = StepSource(this.context) + + val value = withTimeoutOrNull(10000) { + source.events.receive() + } + if (value == null || value == 0f) { + Log.d(TAG, "No new steps registered ($value)") + return Result.success() + } + Log.d(TAG, "New steps registered: $value") + coroutineContext + val step = repo.todayStep().first() + if (step != null) { + step.value += value + repo.updateStep(step) + } else { + repo.addStep(Step(value = value)) + } + + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dzeio/openhealth/services/WaterReminderService.kt b/app/src/main/java/com/dzeio/openhealth/workers/WaterReminderService.kt similarity index 95% rename from app/src/main/java/com/dzeio/openhealth/services/WaterReminderService.kt rename to app/src/main/java/com/dzeio/openhealth/workers/WaterReminderService.kt index 97ce28d..3466bdc 100644 --- a/app/src/main/java/com/dzeio/openhealth/services/WaterReminderService.kt +++ b/app/src/main/java/com/dzeio/openhealth/workers/WaterReminderService.kt @@ -1,9 +1,8 @@ -package com.dzeio.openhealth.services +package com.dzeio.openhealth.workers import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context -import android.content.Intent import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat @@ -12,12 +11,11 @@ import androidx.navigation.NavDeepLinkBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkerParameters import com.dzeio.openhealth.Application -import com.dzeio.openhealth.MainActivity import com.dzeio.openhealth.R import com.dzeio.openhealth.core.BaseService import com.dzeio.openhealth.interfaces.NotificationChannels import com.dzeio.openhealth.interfaces.NotificationIds -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit class WaterReminderService( diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a6ac584..eedec14 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -14,7 +14,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:context=".MainActivity"> + tools:context=".ui.MainActivity"> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index ee80a64..8043ba8 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -168,6 +168,9 @@ + @@ -176,4 +179,9 @@ android:name="com.dzeio.openhealth.ui.activity.ActivityFragment" android:label="@string/menu_activity" tools:layout="@layout/fragment_activity" /> + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8d4c2fe..d7a4dcd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -37,5 +37,7 @@ Activity Ajouter un objectif Modifier l\'objectif + Modifier le but journalier + Vous avez décliné une permission, vous ne pouvez pas utiliser cette extension suaf si vous réactivez la permission manuellement diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc3235a..0e41f7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,4 +48,7 @@ Add Goal Modify Goal + You declined a permission, you can\'t use this extension unless you enable it manually + Modifiy daily goal + Steps