1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-04-23 11:22:10 +00:00

feat: Add Steps counter to app

This commit is contained in:
Florian Bouillon 2022-07-18 18:26:50 +02:00
parent 82d6b04db2
commit c4c6e45687
33 changed files with 815 additions and 270 deletions

View File

@ -101,11 +101,14 @@ dependencies {
implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'javax.inject:javax.inject:1' 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.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-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 // Settings
implementation "androidx.preference:preference-ktx:1.2.0" 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-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-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 // Services
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
@ -128,8 +136,8 @@ dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
// Hilt // Hilt
implementation "com.google.dagger:hilt-android:2.40.5" implementation 'com.google.dagger:hilt-android:2.42'
kapt "com.google.dagger:hilt-compiler:2.40.5" kapt 'com.google.dagger:hilt-compiler:2.42'
// Google Fit // Google Fit
implementation "com.google.android.gms:play-services-fitness:21.1.0" implementation "com.google.android.gms:play-services-fitness:21.1.0"
@ -137,11 +145,16 @@ dependencies {
// Samsung Health // Samsung Health
implementation files('libs/samsung-health-data-1.5.0.aar') 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 // ROOM
implementation "androidx.room:room-runtime:2.4.2" implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2" kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.room:room-ktx:2.4.2" implementation "androidx.room:room-ktx:2.4.2"
testImplementation "androidx.room:room-testing: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'
} }

View File

@ -34,7 +34,7 @@
android:value="@integer/google_play_services_version" /> android:value="@integer/google_play_services_version" />
<activity <activity
android:name=".MainActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.OpenHealth.NoActionBar"> android:theme="@style/Theme.OpenHealth.NoActionBar">
<intent-filter> <intent-filter>
@ -43,6 +43,11 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".services.OpenHealthService"
android:permission="android.permission.ACTIVITY_RECOGNITION"
android:exported="false"/>
</application> </application>
</manifest> </manifest>

View File

@ -1,9 +1,7 @@
package com.dzeio.openhealth package com.dzeio.openhealth
import android.app.Application import android.app.Application
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@ -27,9 +25,15 @@ class Application : Application() {
val locale = Locale(lang) val locale = Locale(lang)
Locale.setDefault(locale) Locale.setDefault(locale)
val overrideConfiguration = baseContext.resources.configuration // val overrideConfiguration = baseContext.resources.configuration
overrideConfiguration.locale = locale // overrideConfiguration.locale = locale
val context: Context = createConfigurationContext(overrideConfiguration) // val context: Context = createConfigurationContext(overrideConfiguration)
val resources: Resources = context.getResources() // val resources: Resources = context.resources
}
/**
* SharedPreferences Key
*/
object Settings {
} }
} }

View File

@ -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<Step, LayoutItemListBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> LayoutItemListBinding
get() = LayoutItemListBinding::inflate
var onItemClick: ((weight: Step) -> Unit)? = null
override fun onBindData(
holder: BaseViewHolder<LayoutItemListBinding>,
item: Step,
position: Int
) {
holder.binding.value.text = "${item.value}ml"
holder.binding.datetime.text = item.formatTimestamp()
holder.binding.edit.setOnClickListener {
onItemClick?.invoke(item)
}
}
}

View File

@ -32,7 +32,7 @@ abstract class BaseAdapter<T, VB : ViewBinding> : RecyclerView.Adapter<BaseViewH
abstract fun onBindData(holder: BaseViewHolder<VB>, item: T, position: Int) abstract fun onBindData(holder: BaseViewHolder<VB>, item: T, position: Int)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<VB> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<VB> {
return BaseViewHolder<VB>( return BaseViewHolder(
bindingInflater(LayoutInflater.from(parent.context), parent, false) bindingInflater(LayoutInflater.from(parent.context), parent, false)
) )
} }

View File

@ -2,24 +2,20 @@ package com.dzeio.openhealth.core
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkRequest
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
abstract class BaseService(context: Context, params: WorkerParameters) : Worker(context, params) { abstract class BaseService(context: Context, params: WorkerParameters) : Worker(context, params) {
companion object { companion object {
fun schedule(tag: String, request: WorkRequest, context: Context) { fun schedule(tag: String, request: PeriodicWorkRequest, context: Context) {
WorkManager.getInstance(context)
.cancelAllWorkByTag(tag)
Log.d("OpenHealth/BaseService", "Scheduled Job $tag") Log.d("OpenHealth/BaseService", "Scheduled Job $tag")
WorkManager.getInstance(context) WorkManager.getInstance(context)
.enqueue(request) .enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.KEEP, request)
} }
} }
} }

View File

@ -4,6 +4,8 @@ import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase 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.Water
import com.dzeio.openhealth.data.water.WaterDao import com.dzeio.openhealth.data.water.WaterDao
import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.data.weight.Weight
@ -12,7 +14,8 @@ import com.dzeio.openhealth.data.weight.WeightDao
@Database( @Database(
entities = [ entities = [
Weight::class, Weight::class,
Water::class Water::class,
Step::class
], ],
version = 1, version = 1,
exportSchema = false exportSchema = false
@ -23,6 +26,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun weightDao(): WeightDao abstract fun weightDao(): WeightDao
abstract fun waterDao(): WaterDao abstract fun waterDao(): WaterDao
abstract fun stepDao(): StepDao
companion object { companion object {
private const val DATABASE_NAME = "open_health" private const val DATABASE_NAME = "open_health"

View File

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

View File

@ -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<Step> {
@Query("SELECT * FROM Step ORDER BY timestamp DESC")
fun getAll(): Flow<List<Step>>
@Query("SELECT * FROM Step where id = :weightId")
fun getOne(weightId: Long): Flow<Step?>
@Query("Select count(*) from Step")
fun getCount(): Flow<Int>
@Query("Select * FROM Step ORDER BY timestamp DESC LIMIT 1")
fun last(): Flow<Step?>
@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?
}

View File

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

View File

@ -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<Float>(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")
}
}

View File

@ -1,8 +1,8 @@
package com.dzeio.openhealth.data.weight package com.dzeio.openhealth.data.weight
import androidx.room.* import androidx.room.Dao
import androidx.room.Query
import com.dzeio.openhealth.core.BaseDao import com.dzeio.openhealth.core.BaseDao
import dagger.Provides
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@ -11,10 +11,9 @@ interface WeightDao : BaseDao<Weight> {
@Query("SELECT * FROM Weight ORDER BY timestamp") @Query("SELECT * FROM Weight ORDER BY timestamp")
fun getAll(): Flow<List<Weight>> fun getAll(): Flow<List<Weight>>
@Query("SELECT * FROM Weight where id = :weightId") @Query("SELECT * FROM Weight WHERE id = :weightId")
fun getOne(weightId: Long): Flow<Weight?> fun getOne(weightId: Long): Flow<Weight?>
@Query("Select count(*) from Weight") @Query("Select count(*) from Weight")
fun getCount(): Flow<Int> fun getCount(): Flow<Int>

View File

@ -2,6 +2,7 @@ package com.dzeio.openhealth.di
import android.content.Context import android.content.Context
import com.dzeio.openhealth.data.AppDatabase 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.water.WaterDao
import com.dzeio.openhealth.data.weight.WeightDao import com.dzeio.openhealth.data.weight.WeightDao
import dagger.Module import dagger.Module
@ -30,4 +31,9 @@ class DatabaseModule {
fun provideWaterDao(appDatabase: AppDatabase): WaterDao { fun provideWaterDao(appDatabase: AppDatabase): WaterDao {
return appDatabase.waterDao() return appDatabase.waterDao()
} }
@Provides
fun provideStepsDao(appDatabase: AppDatabase): StepDao {
return appDatabase.stepDao()
}
} }

View File

@ -9,9 +9,9 @@ import com.dzeio.openhealth.data.weight.Weight
/** /**
* Extension Schema * Extension Schema
* *
* Version: 1.0.0 * Version: 0.1.0
*/ */
abstract class Extension { interface Extension {
data class ImportState<T>( data class ImportState<T>(
val state: States = States.WIP, val state: States = States.WIP,
@ -33,6 +33,8 @@ abstract class Extension {
STEPS STEPS
/** /**
* Google Fit:
*
* STEP_COUNT_CUMULATIVE * STEP_COUNT_CUMULATIVE
* ACTIVITY_SEGMENT * ACTIVITY_SEGMENT
* SLEEP_SEGMENT * SLEEP_SEGMENT
@ -44,47 +46,52 @@ abstract class Extension {
*/ */
} }
val permissions: Array<String>?
val permissionsText: String?
/** /**
* the Source ID * the Source ID
* *
* DO NOT CHANGE IT AFTER THE EXTENSION IS IN PRODUCTION * DO NOT CHANGE IT AFTER THE EXTENSION IS IN PRODUCTION
*/ */
abstract val id: String val id: String
/** /**
* The Extension Display Name * The Extension Display Name
*/ */
abstract val name: String val name: String
/** /**
* Initialize hte Extension * Initialize hte Extension
* *
* It is run Before any functions is launched and after events handlers are set * It is run Before any functions is launched and after events handlers are set
*/ */
abstract fun init(activity: Activity): Array<Data> fun init(activity: Activity): Array<Data>
/** /**
* A status shown on the extension list page * A status shown on the extension list page
*/ */
open fun getStatus(): String { fun getStatus(): String {
return "No Status set..." return "No Status set..."
} }
/** /**
* Function that will check * 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<States> { fun connect(): LiveData<States> {
return MutableLiveData(States.DONE) return MutableLiveData(States.DONE)
} }
open fun importWeight(): LiveData<ImportState<Weight>> { fun importWeight(): LiveData<ImportState<Weight>> {
return MutableLiveData(ImportState(States.DONE)) return MutableLiveData(ImportState(States.DONE))
} }
@ -92,7 +99,7 @@ abstract class Extension {
* function run when outgoing sync is enabled and new value is added * function run when outgoing sync is enabled and new value is added
* or manual export is launched * or manual export is launched
*/ */
open fun exportWeight(weight: Weight): LiveData<States> { fun exportWeight(weight: Weight): LiveData<States> {
return MutableLiveData(States.DONE) return MutableLiveData(States.DONE)
} }
@ -100,20 +107,8 @@ abstract class Extension {
* Activity Events * 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<String>,
grantResult: IntArray
) {
}
/** /**
* Same as Activity/Fragment onActivityResult * Same as Activity/Fragment onActivityResult
*/ */
open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {} fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {}
} }

View File

@ -3,10 +3,7 @@ package com.dzeio.openhealth.extensions
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.dzeio.openhealth.data.weight.Weight import com.dzeio.openhealth.data.weight.Weight
@ -21,7 +18,7 @@ import java.util.Date
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class GoogleFit() : Extension() { class GoogleFit: Extension {
companion object { companion object {
const val TAG = "GoogleFitConnector" const val TAG = "GoogleFitConnector"
} }
@ -31,10 +28,16 @@ class GoogleFit() : Extension() {
override val id = "GoogleFit" override val id = "GoogleFit"
override val name = "Google Fit" override val name = "Google Fit"
override fun init(activity: Activity): Array<Data> { override val permissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
override val permissionsText: String = "Please"
override fun init(activity: Activity): Array<Extension.Data> {
this.activity = activity this.activity = activity
return arrayOf( return arrayOf(
Data.WEIGHT Extension.Data.WEIGHT
) )
} }
@ -55,47 +58,14 @@ class GoogleFit() : Extension() {
// .addDataType(DataType.TYPE_CALORIES_EXPENDED) // .addDataType(DataType.TYPE_CALORIES_EXPENDED)
.build() .build()
// private fun checkPermissionsAndRun(data: Data) { private val connectLiveData: MutableLiveData<Extension.States> = MutableLiveData(Extension.States.WIP)
// 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 fun permissionApproved(): Boolean = override fun connect(): LiveData<Extension.States> {
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<States> = MutableLiveData(States.WIP)
override fun connect(): LiveData<States> {
if (!permissionApproved()) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
87531
)
return connectLiveData
}
if (isConnected()) { if (isConnected()) {
connectLiveData.value = States.DONE connectLiveData.value = Extension.States.DONE
} else { } else {
Log.d("GoogleFitImporter", "Signing In") Log.d(this.name, "Signing In")
GoogleSignIn.requestPermissions( GoogleSignIn.requestPermissions(
activity, activity,
124887, 124887,
@ -105,19 +75,6 @@ class GoogleFit() : Extension() {
return connectLiveData 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 fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
private val timeRange by lazy { private val timeRange by lazy {
@ -131,7 +88,7 @@ class GoogleFit() : Extension() {
return@lazy arrayOf(startTime, endTime) return@lazy arrayOf(startTime, endTime)
} }
private fun startImport(data: Data) { private fun startImport(data: Extension.Data) {
Log.d("GoogleFitImporter", "Importing for ${data.name}") Log.d("GoogleFitImporter", "Importing for ${data.name}")
val dateFormat = DateFormat.getDateInstance() val dateFormat = DateFormat.getDateInstance()
@ -142,7 +99,7 @@ class GoogleFit() : Extension() {
var timeUnit = TimeUnit.MILLISECONDS var timeUnit = TimeUnit.MILLISECONDS
when (data) { when (data) {
Data.STEPS -> { Extension.Data.STEPS -> {
type = DataType.TYPE_STEP_COUNT_CUMULATIVE type = DataType.TYPE_STEP_COUNT_CUMULATIVE
} }
else -> {} 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( Fitness.getHistoryClient(
activity, activity,
GoogleSignIn.getAccountForExtension(activity, fitnessOptions) GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
@ -191,7 +148,7 @@ class GoogleFit() : Extension() {
for (field in dp.dataType.fields) { for (field in dp.dataType.fields) {
Log.i(TAG, "\tField: ${field.name} Value: ${dp.getValue(field)}") Log.i(TAG, "\tField: ${field.name} Value: ${dp.getValue(field)}")
when (data) { when (data) {
Data.WEIGHT -> { Extension.Data.WEIGHT -> {
val weight = Weight() val weight = Weight()
weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS) weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS)
weight.weight = dp.getValue(field).asFloat() weight.weight = dp.getValue(field).asFloat()
@ -200,17 +157,17 @@ class GoogleFit() : Extension() {
list.add(weight) list.add(weight)
weightLiveData.value = weightLiveData.value =
ImportState(States.WIP, list) Extension.ImportState(Extension.States.WIP, list)
} }
else -> {} else -> {}
} }
} }
} }
when (data) { when (data) {
Data.WEIGHT -> { Extension.Data.WEIGHT -> {
weightLiveData.value = weightLiveData.value =
ImportState( Extension.ImportState(
States.DONE, Extension.States.DONE,
weightLiveData.value?.list weightLiveData.value?.list
?: ArrayList() ?: ArrayList()
) )
@ -224,39 +181,27 @@ class GoogleFit() : Extension() {
} }
} }
/**
* Currently not usable
*/
override fun onRequestPermissionResult(
requestCode: Int,
permission: Array<String>,
grantResult: IntArray
) {
connect()
// signIn(Data.values()[requestCode])
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d(this.name, "[$requestCode] -> [$resultCode]: $data")
if (requestCode == 0) { if (requestCode == 0) {
return return
} }
connectLiveData.value = States.DONE
if (resultCode == Activity.RESULT_OK) connectLiveData.value = Extension.States.DONE
// signIn(Data.values()[requestCode]) // signIn(Data.values()[requestCode])
} }
private lateinit var weightLiveData: MutableLiveData<ImportState<Weight>> private lateinit var weightLiveData: MutableLiveData<Extension.ImportState<Weight>>
override fun importWeight(): LiveData<ImportState<Weight>> { override fun importWeight(): LiveData<Extension.ImportState<Weight>> {
weightLiveData = MutableLiveData( weightLiveData = MutableLiveData(
ImportState( Extension.ImportState(
States.WIP Extension.States.WIP
) )
) )
startImport(Data.WEIGHT) startImport(Extension.Data.WEIGHT)
// checkPermissionsAndRun(Data.WEIGHT)
return weightLiveData return weightLiveData
} }

View File

@ -1,10 +1,12 @@
package com.dzeio.openhealth.interfaces package com.dzeio.openhealth.interfaces
import android.app.NotificationManager
enum class NotificationChannels( enum class NotificationChannels(
val id: String, val id: String,
val channelName: String, val channelName: String,
val importance: Int val importance: Int
) { ) {
// 3 is IMPORTANCE_DEFAULT WATER("water", "Water Notifications", NotificationManager.IMPORTANCE_DEFAULT),
WATER("water", "Water Notifications", 3) SERVICE("service", "Open Health Service", NotificationManager.IMPORTANCE_MIN)
} }

View File

@ -1,5 +1,10 @@
package com.dzeio.openhealth.interfaces package com.dzeio.openhealth.interfaces
enum class NotificationIds { enum class NotificationIds {
WaterIntake WaterIntake,
/**
* Open Health Main Service Notification ID
*/
Service
} }

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.dzeio.openhealth package com.dzeio.openhealth.ui
import android.app.ActivityManager
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
@ -17,16 +18,22 @@ import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.navigateUp import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController 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.core.BaseActivity
import com.dzeio.openhealth.databinding.ActivityMainBinding import com.dzeio.openhealth.databinding.ActivityMainBinding
import com.dzeio.openhealth.interfaces.NotificationChannels 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 import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : BaseActivity<ActivityMainBinding>() { class MainActivity : BaseActivity<ActivityMainBinding>() {
companion object {
const val TAG = "${Application.TAG}/MainActivity"
}
private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var navController: NavController private lateinit var navController: NavController
@ -57,21 +64,30 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
binding.bottomNav.setupWithNavController(navController) binding.bottomNav.setupWithNavController(navController)
// binding.bottomNav.setOnItemSelectedListener { // registerForActivityResult(ActivityResultContracts.RequestPermission()) {
// val currentFragment = supportFragmentManager.fragments.last()
// // currentFragment.javaClass.canonicalName
// //
// navController.
//
// false
// } // }
// .launch(Manifest.permission.ACTIVITY_RECOGNITION)
createNotificationChannel() createNotificationChannel()
// Services // Services
WorkManager.getInstance(this)
.cancelAllWork()
WaterReminderService.setup(this) WaterReminderService.setup(this)
// StepCountService.setup(this)
this.betterStartService(OpenHealthService::class.java)
}
private fun <T> betterStartService(service: Class<T>) {
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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -87,18 +103,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
NavigationUI.onNavDestinationSelected(item, navController) || NavigationUI.onNavDestinationSelected(item, navController) ||
super.onOptionsItemSelected(item) super.onOptionsItemSelected(item)
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
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") @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)

View File

@ -32,5 +32,9 @@ class BrowseFragment :
binding.waterIntake.setOnClickListener { binding.waterIntake.setOnClickListener {
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome()) findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome())
} }
binding.steps.setOnClickListener {
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToStepsHomeFragment())
}
} }
} }

View File

@ -1,22 +1,22 @@
package com.dzeio.openhealth.ui.extensions package com.dzeio.openhealth.ui.extensions
import android.app.ProgressDialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.RequiresApi import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.dzeio.openhealth.R
import com.dzeio.openhealth.adapters.ExtensionAdapter import com.dzeio.openhealth.adapters.ExtensionAdapter
import com.dzeio.openhealth.core.BaseFragment import com.dzeio.openhealth.core.BaseFragment
import com.dzeio.openhealth.databinding.FragmentExtensionsBinding import com.dzeio.openhealth.databinding.FragmentExtensionsBinding
import com.dzeio.openhealth.extensions.Extension import com.dzeio.openhealth.extensions.Extension
import com.dzeio.openhealth.extensions.GoogleFit import com.dzeio.openhealth.extensions.GoogleFit
import com.dzeio.openhealth.utils.PermissionsManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
@ -32,6 +32,16 @@ class ExtensionsFragment :
private lateinit var activeExtension: Extension private lateinit var activeExtension: Extension
private val activityResult = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { map ->
if (map.containsValue(false)) {
// TODO: Show a popup with choice to change it
Toast.makeText(requireContext(), R.string.permission_declined, Toast.LENGTH_LONG).show()
return@registerForActivityResult
}
extensionIsConnected(activeExtension)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -43,20 +53,9 @@ class ExtensionsFragment :
val adapter = ExtensionAdapter() val adapter = ExtensionAdapter()
adapter.onItemClick = { adapter.onItemClick = {
activeExtension = it activeExtension = it
Log.d(it.id, it.name) Log.d(TAG, "${it.id}: ${it.name}")
if (it.isConnected()) {
Log.d(it.id, "Continue!") this.extensionPermissionsVerified(it)
findNavController().navigate(
ExtensionsFragmentDirections.actionNavExtensionsToNavExtension(
it.id
)
)
} else {
val ls = it.connect()
ls.observe(viewLifecycleOwner) { st ->
Log.d("States", st.name)
}
}
} }
recycler.adapter = adapter recycler.adapter = adapter
@ -71,56 +70,43 @@ class ExtensionsFragment :
adapter.set(list) adapter.set(list)
} }
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
activeExtension.onActivityResult(requestCode, resultCode, data) activeExtension.onActivityResult(requestCode, resultCode, data)
} }
@RequiresApi(Build.VERSION_CODES.O) private fun extensionPermissionsVerified(it: Extension) {
override fun onRequestPermissionsResult( // Check for the extension permissions
requestCode: Int, permissions: Array<String>, if (it.permissions != null && !PermissionsManager.hasPermission(requireContext(), it.permissions!!)) {
grantResults: IntArray // TODO: show popup explaining the permissions requested
) {
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.")
}
grantResults[0] == PackageManager.PERMISSION_GRANTED -> { // show permissions
Log.d(TAG, "Granted") activityResult.launch(it.permissions)
activeExtension.onRequestPermissionResult(requestCode, permissions, grantResults) return
} }
else -> { extensionIsConnected(it)
// Permission denied. }
// In this Activity we've chosen to notify the user that they private fun extensionIsConnected(it: Extension) {
// have rejected a core permission for the app since it makes the Activity useless. // check if it is connected
// We're communicating this message in a Snackbar since this is a sample app, but if (it.isConnected()) {
// core permissions would typically be best requested during a welcome-screen flow. gotoExtension(it)
return
}
// Additionally, it is important to remember that a permission might have been // IDK: maybe give less liberty to the extension IDK
// rejected without asking the user for permission (device policy or "Never ask val ld = it.connect()
// again" prompts). Therefore, a user interface affordance is typically implemented ld.observe(viewLifecycleOwner) { state ->
// when permissions are denied. Otherwise, your app could appear unresponsive to Log.d(TAG, state.toString())
// touches or interactions which have required permissions. if (state == Extension.States.DONE) {
Log.e(TAG, "Error") gotoExtension(it)
// 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()
} }
} }
} }
private fun gotoExtension(it: Extension) {
findNavController().navigate(
ExtensionsFragmentDirections.actionNavExtensionsToNavExtension(it.id)
)
}
} }

View File

@ -6,7 +6,6 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.RectF import android.graphics.RectF
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -24,9 +23,9 @@ import com.dzeio.openhealth.utils.DrawUtils
import com.dzeio.openhealth.utils.GraphUtils import com.dzeio.openhealth.utils.GraphUtils
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.min
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlin.math.max import kotlin.math.max
import kotlin.math.min
@AndroidEntryPoint @AndroidEntryPoint
class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewModel::class.java) { class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewModel::class.java) {
@ -204,7 +203,7 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
animator.addUpdateListener { animator.addUpdateListener {
this.oldValue = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat() this.oldValue = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat()
Log.d("Test2", "${this.oldValue}") // Log.d("Test2", "${this.oldValue}")
DrawUtils.drawArc( DrawUtils.drawArc(
canvas, canvas,

View File

@ -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, FragmentStepsHomeBinding>(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()
}
}
}

View File

@ -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<List<Step>> = MutableLiveData()
fun init() {
viewModelScope.launch {
stepRepository.getSteps().collectLatest {
items.postValue(it)
}
}
}
}

View File

@ -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<String>): Boolean {
for (permission in permissions) {
val res = hasPermission(context, permission)
if (!res) {
return false
}
}
return true
}
}

View File

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

View File

@ -1,9 +1,8 @@
package com.dzeio.openhealth.services package com.dzeio.openhealth.workers
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
@ -12,12 +11,11 @@ import androidx.navigation.NavDeepLinkBuilder
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.dzeio.openhealth.Application import com.dzeio.openhealth.Application
import com.dzeio.openhealth.MainActivity
import com.dzeio.openhealth.R import com.dzeio.openhealth.R
import com.dzeio.openhealth.core.BaseService import com.dzeio.openhealth.core.BaseService
import com.dzeio.openhealth.interfaces.NotificationChannels import com.dzeio.openhealth.interfaces.NotificationChannels
import com.dzeio.openhealth.interfaces.NotificationIds import com.dzeio.openhealth.interfaces.NotificationIds
import java.util.* import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class WaterReminderService( class WaterReminderService(

View File

@ -14,7 +14,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:context=".MainActivity"> tools:context=".ui.MainActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.water.WaterHomeFragment">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="200dp"
android:minHeight="200dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:gravity="end"
android:orientation="horizontal"
android:weightSum="2">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_edit_default_intake"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
style="?attr/materialButtonOutlinedStyle"
android:text="@string/edit_daily_goal" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:clipToPadding="false"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:listitem="@layout/layout_item_list" />
</LinearLayout>

View File

@ -168,6 +168,9 @@
<action <action
android:id="@+id/action_nav_browse_to_nav_list_weight" android:id="@+id/action_nav_browse_to_nav_list_weight"
app:destination="@id/nav_list_weight" /> app:destination="@id/nav_list_weight" />
<action
android:id="@+id/action_nav_browse_to_stepsHomeFragment"
app:destination="@id/stepsHomeFragment" />
</fragment> </fragment>
@ -176,4 +179,9 @@
android:name="com.dzeio.openhealth.ui.activity.ActivityFragment" android:name="com.dzeio.openhealth.ui.activity.ActivityFragment"
android:label="@string/menu_activity" android:label="@string/menu_activity"
tools:layout="@layout/fragment_activity" /> tools:layout="@layout/fragment_activity" />
<fragment
android:id="@+id/stepsHomeFragment"
android:name="com.dzeio.openhealth.ui.steps.StepsHomeFragment"
android:label="@string/menu_steps"
tools:layout="@layout/fragment_steps_home" />
</navigation> </navigation>

View File

@ -37,5 +37,7 @@
<string name="menu_activity">Activity</string> <string name="menu_activity">Activity</string>
<string name="add_goal">Ajouter un objectif</string> <string name="add_goal">Ajouter un objectif</string>
<string name="edit_goal">Modifier l\'objectif</string> <string name="edit_goal">Modifier l\'objectif</string>
<string name="edit_daily_goal">Modifier le but journalier</string>
<string name="permission_declined">Vous avez décliné une permission, vous ne pouvez pas utiliser cette extension suaf si vous réactivez la permission manuellement</string>
</resources> </resources>

View File

@ -48,4 +48,7 @@
<string name="add_goal">Add Goal</string> <string name="add_goal">Add Goal</string>
<string name="edit_goal">Modify Goal</string> <string name="edit_goal">Modify Goal</string>
<string name="permission_declined">You declined a permission, you can\'t use this extension unless you enable it manually</string>
<string name="edit_daily_goal">Modifiy daily goal</string>
<string name="menu_steps">Steps</string>
</resources> </resources>