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:
parent
82d6b04db2
commit
c4c6e45687
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"
|
||||||
|
45
app/src/main/java/com/dzeio/openhealth/data/step/Step.kt
Normal file
45
app/src/main/java/com/dzeio/openhealth/data/step/Step.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
35
app/src/main/java/com/dzeio/openhealth/data/step/StepDao.kt
Normal file
35
app/src/main/java/com/dzeio/openhealth/data/step/StepDao.kt
Normal 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?
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
@ -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?) {}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
@ -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"
|
||||||
|
61
app/src/main/res/layout/fragment_steps_home.xml
Normal file
61
app/src/main/res/layout/fragment_steps_home.xml
Normal 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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user