mirror of
https://github.com/dzeiocom/OpenHealth.git
synced 2025-04-23 03:12:14 +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.appcompat:appcompat:1.4.2'
|
||||
implementation 'javax.inject:javax.inject:1'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha02'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha03'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0'
|
||||
|
||||
// Coroutines
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3"
|
||||
|
||||
// Settings
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
|
||||
@ -116,6 +119,11 @@ dependencies {
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime:3.1.1"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
|
||||
|
||||
|
||||
// Services
|
||||
implementation 'androidx.work:work-runtime-ktx:2.7.1'
|
||||
|
||||
@ -128,8 +136,8 @@ dependencies {
|
||||
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
|
||||
|
||||
// Hilt
|
||||
implementation "com.google.dagger:hilt-android:2.40.5"
|
||||
kapt "com.google.dagger:hilt-compiler:2.40.5"
|
||||
implementation 'com.google.dagger:hilt-android:2.42'
|
||||
kapt 'com.google.dagger:hilt-compiler:2.42'
|
||||
|
||||
// Google Fit
|
||||
implementation "com.google.android.gms:play-services-fitness:21.1.0"
|
||||
@ -137,11 +145,16 @@ dependencies {
|
||||
|
||||
// Samsung Health
|
||||
implementation files('libs/samsung-health-data-1.5.0.aar')
|
||||
implementation "com.google.code.gson:gson:2.8.9"
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
|
||||
// ROOM
|
||||
implementation "androidx.room:room-runtime:2.4.2"
|
||||
kapt "androidx.room:room-compiler:2.4.2"
|
||||
implementation "androidx.room:room-ktx:2.4.2"
|
||||
testImplementation "androidx.room:room-testing:2.4.2"
|
||||
|
||||
// Futures
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation "androidx.concurrent:concurrent-futures:1.1.0"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.3'
|
||||
}
|
||||
|
@ -34,7 +34,7 @@
|
||||
android:value="@integer/google_play_services_version" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.OpenHealth.NoActionBar">
|
||||
<intent-filter>
|
||||
@ -43,6 +43,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".services.OpenHealthService"
|
||||
android:permission="android.permission.ACTIVITY_RECOGNITION"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,9 +1,7 @@
|
||||
package com.dzeio.openhealth
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
@ -27,9 +25,15 @@ class Application : Application() {
|
||||
val locale = Locale(lang)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val overrideConfiguration = baseContext.resources.configuration
|
||||
overrideConfiguration.locale = locale
|
||||
val context: Context = createConfigurationContext(overrideConfiguration)
|
||||
val resources: Resources = context.getResources()
|
||||
// val overrideConfiguration = baseContext.resources.configuration
|
||||
// overrideConfiguration.locale = locale
|
||||
// val context: Context = createConfigurationContext(overrideConfiguration)
|
||||
// val resources: Resources = context.resources
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedPreferences Key
|
||||
*/
|
||||
object Settings {
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<VB> {
|
||||
return BaseViewHolder<VB>(
|
||||
return BaseViewHolder(
|
||||
bindingInflater(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
@ -2,24 +2,20 @@ package com.dzeio.openhealth.core
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkRequest
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
abstract class BaseService(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
fun schedule(tag: String, request: WorkRequest, context: Context) {
|
||||
WorkManager.getInstance(context)
|
||||
.cancelAllWorkByTag(tag)
|
||||
|
||||
fun schedule(tag: String, request: PeriodicWorkRequest, context: Context) {
|
||||
Log.d("OpenHealth/BaseService", "Scheduled Job $tag")
|
||||
WorkManager.getInstance(context)
|
||||
.enqueue(request)
|
||||
|
||||
.enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@ import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.dzeio.openhealth.data.step.Step
|
||||
import com.dzeio.openhealth.data.step.StepDao
|
||||
import com.dzeio.openhealth.data.water.Water
|
||||
import com.dzeio.openhealth.data.water.WaterDao
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
@ -12,7 +14,8 @@ import com.dzeio.openhealth.data.weight.WeightDao
|
||||
@Database(
|
||||
entities = [
|
||||
Weight::class,
|
||||
Water::class
|
||||
Water::class,
|
||||
Step::class
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
@ -23,6 +26,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun weightDao(): WeightDao
|
||||
abstract fun waterDao(): WaterDao
|
||||
abstract fun stepDao(): StepDao
|
||||
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "open_health"
|
||||
|
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
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.dzeio.openhealth.core.BaseDao
|
||||
import dagger.Provides
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@ -11,10 +11,9 @@ interface WeightDao : BaseDao<Weight> {
|
||||
@Query("SELECT * FROM Weight ORDER BY timestamp")
|
||||
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?>
|
||||
|
||||
|
||||
@Query("Select count(*) from Weight")
|
||||
fun getCount(): Flow<Int>
|
||||
|
||||
|
@ -2,6 +2,7 @@ package com.dzeio.openhealth.di
|
||||
|
||||
import android.content.Context
|
||||
import com.dzeio.openhealth.data.AppDatabase
|
||||
import com.dzeio.openhealth.data.step.StepDao
|
||||
import com.dzeio.openhealth.data.water.WaterDao
|
||||
import com.dzeio.openhealth.data.weight.WeightDao
|
||||
import dagger.Module
|
||||
@ -30,4 +31,9 @@ class DatabaseModule {
|
||||
fun provideWaterDao(appDatabase: AppDatabase): WaterDao {
|
||||
return appDatabase.waterDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideStepsDao(appDatabase: AppDatabase): StepDao {
|
||||
return appDatabase.stepDao()
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ import com.dzeio.openhealth.data.weight.Weight
|
||||
/**
|
||||
* Extension Schema
|
||||
*
|
||||
* Version: 1.0.0
|
||||
* Version: 0.1.0
|
||||
*/
|
||||
abstract class Extension {
|
||||
interface Extension {
|
||||
|
||||
data class ImportState<T>(
|
||||
val state: States = States.WIP,
|
||||
@ -33,6 +33,8 @@ abstract class Extension {
|
||||
STEPS
|
||||
|
||||
/**
|
||||
* Google Fit:
|
||||
*
|
||||
* STEP_COUNT_CUMULATIVE
|
||||
* ACTIVITY_SEGMENT
|
||||
* SLEEP_SEGMENT
|
||||
@ -44,47 +46,52 @@ abstract class Extension {
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
val permissions: Array<String>?
|
||||
|
||||
val permissionsText: String?
|
||||
|
||||
/**
|
||||
* the Source ID
|
||||
*
|
||||
* DO NOT CHANGE IT AFTER THE EXTENSION IS IN PRODUCTION
|
||||
*/
|
||||
abstract val id: String
|
||||
val id: String
|
||||
|
||||
/**
|
||||
* The Extension Display Name
|
||||
*/
|
||||
abstract val name: String
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* Initialize hte Extension
|
||||
*
|
||||
* It is run Before any functions is launched and after events handlers are set
|
||||
*/
|
||||
abstract fun init(activity: Activity): Array<Data>
|
||||
fun init(activity: Activity): Array<Data>
|
||||
|
||||
/**
|
||||
* A status shown on the extension list page
|
||||
*/
|
||||
open fun getStatus(): String {
|
||||
fun getStatus(): String {
|
||||
return "No Status set..."
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that will check
|
||||
*/
|
||||
abstract fun isAvailable(): Boolean
|
||||
fun isAvailable(): Boolean
|
||||
|
||||
/**
|
||||
* idk
|
||||
* return if the extension is already connected to remote of not
|
||||
*/
|
||||
abstract fun isConnected(): Boolean
|
||||
fun isConnected(): Boolean
|
||||
|
||||
open fun connect(): LiveData<States> {
|
||||
fun connect(): LiveData<States> {
|
||||
return MutableLiveData(States.DONE)
|
||||
}
|
||||
|
||||
open fun importWeight(): LiveData<ImportState<Weight>> {
|
||||
fun importWeight(): LiveData<ImportState<Weight>> {
|
||||
return MutableLiveData(ImportState(States.DONE))
|
||||
}
|
||||
|
||||
@ -92,7 +99,7 @@ abstract class Extension {
|
||||
* function run when outgoing sync is enabled and new value is added
|
||||
* or manual export is launched
|
||||
*/
|
||||
open fun exportWeight(weight: Weight): LiveData<States> {
|
||||
fun exportWeight(weight: Weight): LiveData<States> {
|
||||
return MutableLiveData(States.DONE)
|
||||
}
|
||||
|
||||
@ -100,20 +107,8 @@ abstract class Extension {
|
||||
* Activity Events
|
||||
*/
|
||||
|
||||
/**
|
||||
* Same as Activity/Fragment onRequestPermissionResult
|
||||
*
|
||||
* But it will only be launched if grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
*/
|
||||
open fun onRequestPermissionResult(
|
||||
requestCode: Int,
|
||||
permission: Array<String>,
|
||||
grantResult: IntArray
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.dzeio.openhealth.data.weight.Weight
|
||||
@ -21,7 +18,7 @@ import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class GoogleFit() : Extension() {
|
||||
class GoogleFit: Extension {
|
||||
companion object {
|
||||
const val TAG = "GoogleFitConnector"
|
||||
}
|
||||
@ -31,10 +28,16 @@ class GoogleFit() : Extension() {
|
||||
override val id = "GoogleFit"
|
||||
override val name = "Google Fit"
|
||||
|
||||
override fun init(activity: Activity): Array<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
|
||||
return arrayOf(
|
||||
Data.WEIGHT
|
||||
Extension.Data.WEIGHT
|
||||
)
|
||||
}
|
||||
|
||||
@ -55,47 +58,14 @@ class GoogleFit() : Extension() {
|
||||
// .addDataType(DataType.TYPE_CALORIES_EXPENDED)
|
||||
.build()
|
||||
|
||||
// private fun checkPermissionsAndRun(data: Data) {
|
||||
// if (permissionApproved()) {
|
||||
// signIn(data)
|
||||
// } else {
|
||||
// Log.d(TAG, "Asking for permission")
|
||||
// // Ask for permission
|
||||
// ActivityCompat.requestPermissions(
|
||||
// activity,
|
||||
// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||
// data.ordinal
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
private val connectLiveData: MutableLiveData<Extension.States> = MutableLiveData(Extension.States.WIP)
|
||||
|
||||
private fun permissionApproved(): Boolean =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
|
||||
activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
private val connectLiveData: MutableLiveData<States> = MutableLiveData(States.WIP)
|
||||
|
||||
override fun connect(): LiveData<States> {
|
||||
|
||||
if (!permissionApproved()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||
87531
|
||||
)
|
||||
return connectLiveData
|
||||
}
|
||||
override fun connect(): LiveData<Extension.States> {
|
||||
|
||||
if (isConnected()) {
|
||||
connectLiveData.value = States.DONE
|
||||
connectLiveData.value = Extension.States.DONE
|
||||
} else {
|
||||
Log.d("GoogleFitImporter", "Signing In")
|
||||
Log.d(this.name, "Signing In")
|
||||
GoogleSignIn.requestPermissions(
|
||||
activity,
|
||||
124887,
|
||||
@ -105,19 +75,6 @@ class GoogleFit() : Extension() {
|
||||
return connectLiveData
|
||||
}
|
||||
|
||||
// private fun signIn(data: Data) {
|
||||
// if (isConnected()) {
|
||||
// startImport(data)
|
||||
// } else {
|
||||
// Log.d("GoogleFitImporter", "Signing In")
|
||||
// GoogleSignIn.requestPermissions(
|
||||
// activity,
|
||||
// data.ordinal,
|
||||
// getGoogleAccount(), fitnessOptions
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
|
||||
|
||||
private val timeRange by lazy {
|
||||
@ -131,7 +88,7 @@ class GoogleFit() : Extension() {
|
||||
return@lazy arrayOf(startTime, endTime)
|
||||
}
|
||||
|
||||
private fun startImport(data: Data) {
|
||||
private fun startImport(data: Extension.Data) {
|
||||
Log.d("GoogleFitImporter", "Importing for ${data.name}")
|
||||
|
||||
val dateFormat = DateFormat.getDateInstance()
|
||||
@ -142,7 +99,7 @@ class GoogleFit() : Extension() {
|
||||
var timeUnit = TimeUnit.MILLISECONDS
|
||||
|
||||
when (data) {
|
||||
Data.STEPS -> {
|
||||
Extension.Data.STEPS -> {
|
||||
type = DataType.TYPE_STEP_COUNT_CUMULATIVE
|
||||
}
|
||||
else -> {}
|
||||
@ -157,7 +114,7 @@ class GoogleFit() : Extension() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun runRequest(request: DataReadRequest, data: Data) {
|
||||
private fun runRequest(request: DataReadRequest, data: Extension.Data) {
|
||||
Fitness.getHistoryClient(
|
||||
activity,
|
||||
GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
|
||||
@ -191,7 +148,7 @@ class GoogleFit() : Extension() {
|
||||
for (field in dp.dataType.fields) {
|
||||
Log.i(TAG, "\tField: ${field.name} Value: ${dp.getValue(field)}")
|
||||
when (data) {
|
||||
Data.WEIGHT -> {
|
||||
Extension.Data.WEIGHT -> {
|
||||
val weight = Weight()
|
||||
weight.timestamp = dp.getStartTime(TimeUnit.MILLISECONDS)
|
||||
weight.weight = dp.getValue(field).asFloat()
|
||||
@ -200,17 +157,17 @@ class GoogleFit() : Extension() {
|
||||
list.add(weight)
|
||||
weightLiveData.value =
|
||||
|
||||
ImportState(States.WIP, list)
|
||||
Extension.ImportState(Extension.States.WIP, list)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
when (data) {
|
||||
Data.WEIGHT -> {
|
||||
Extension.Data.WEIGHT -> {
|
||||
weightLiveData.value =
|
||||
ImportState(
|
||||
States.DONE,
|
||||
Extension.ImportState(
|
||||
Extension.States.DONE,
|
||||
weightLiveData.value?.list
|
||||
?: ArrayList()
|
||||
)
|
||||
@ -224,39 +181,27 @@ class GoogleFit() : Extension() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently not usable
|
||||
*/
|
||||
override fun onRequestPermissionResult(
|
||||
requestCode: Int,
|
||||
permission: Array<String>,
|
||||
grantResult: IntArray
|
||||
) {
|
||||
connect()
|
||||
// signIn(Data.values()[requestCode])
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
Log.d(this.name, "[$requestCode] -> [$resultCode]: $data")
|
||||
if (requestCode == 0) {
|
||||
return
|
||||
}
|
||||
connectLiveData.value = States.DONE
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) connectLiveData.value = Extension.States.DONE
|
||||
// signIn(Data.values()[requestCode])
|
||||
}
|
||||
|
||||
private lateinit var weightLiveData: MutableLiveData<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(
|
||||
ImportState(
|
||||
States.WIP
|
||||
Extension.ImportState(
|
||||
Extension.States.WIP
|
||||
)
|
||||
)
|
||||
|
||||
startImport(Data.WEIGHT)
|
||||
|
||||
// checkPermissionsAndRun(Data.WEIGHT)
|
||||
startImport(Extension.Data.WEIGHT)
|
||||
|
||||
return weightLiveData
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package com.dzeio.openhealth.interfaces
|
||||
|
||||
import android.app.NotificationManager
|
||||
|
||||
enum class NotificationChannels(
|
||||
val id: String,
|
||||
val channelName: String,
|
||||
val importance: Int
|
||||
) {
|
||||
// 3 is IMPORTANCE_DEFAULT
|
||||
WATER("water", "Water Notifications", 3)
|
||||
WATER("water", "Water Notifications", NotificationManager.IMPORTANCE_DEFAULT),
|
||||
SERVICE("service", "Open Health Service", NotificationManager.IMPORTANCE_MIN)
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
package com.dzeio.openhealth.interfaces
|
||||
|
||||
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.NotificationManager
|
||||
import android.content.Context
|
||||
@ -17,16 +18,22 @@ import androidx.navigation.ui.NavigationUI
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.work.WorkManager
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseActivity
|
||||
import com.dzeio.openhealth.databinding.ActivityMainBinding
|
||||
import com.dzeio.openhealth.interfaces.NotificationChannels
|
||||
import com.dzeio.openhealth.services.WaterReminderService
|
||||
import com.dzeio.openhealth.services.OpenHealthService
|
||||
import com.dzeio.openhealth.workers.WaterReminderService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "${Application.TAG}/MainActivity"
|
||||
}
|
||||
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
|
||||
private lateinit var navController: NavController
|
||||
@ -57,21 +64,30 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
binding.bottomNav.setupWithNavController(navController)
|
||||
|
||||
// binding.bottomNav.setOnItemSelectedListener {
|
||||
// val currentFragment = supportFragmentManager.fragments.last()
|
||||
// // currentFragment.javaClass.canonicalName
|
||||
// registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
//
|
||||
// navController.
|
||||
//
|
||||
// false
|
||||
// }
|
||||
// .launch(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
|
||||
createNotificationChannel()
|
||||
|
||||
// Services
|
||||
WorkManager.getInstance(this)
|
||||
.cancelAllWork()
|
||||
WaterReminderService.setup(this)
|
||||
// StepCountService.setup(this)
|
||||
|
||||
this.betterStartService(OpenHealthService::class.java)
|
||||
}
|
||||
|
||||
private fun <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 {
|
||||
@ -87,18 +103,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
NavigationUI.onNavDestinationSelected(item, navController) ||
|
||||
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")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
@ -32,5 +32,9 @@ class BrowseFragment :
|
||||
binding.waterIntake.setOnClickListener {
|
||||
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToNavWaterHome())
|
||||
}
|
||||
|
||||
binding.steps.setOnClickListener {
|
||||
findNavController().navigate(BrowseFragmentDirections.actionNavBrowseToStepsHomeFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,22 @@
|
||||
package com.dzeio.openhealth.ui.extensions
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.adapters.ExtensionAdapter
|
||||
import com.dzeio.openhealth.core.BaseFragment
|
||||
import com.dzeio.openhealth.databinding.FragmentExtensionsBinding
|
||||
import com.dzeio.openhealth.extensions.Extension
|
||||
import com.dzeio.openhealth.extensions.GoogleFit
|
||||
import com.dzeio.openhealth.utils.PermissionsManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
@ -32,6 +32,16 @@ class ExtensionsFragment :
|
||||
|
||||
private lateinit var activeExtension: Extension
|
||||
|
||||
private val activityResult = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { map ->
|
||||
if (map.containsValue(false)) {
|
||||
// TODO: Show a popup with choice to change it
|
||||
Toast.makeText(requireContext(), R.string.permission_declined, Toast.LENGTH_LONG).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
extensionIsConnected(activeExtension)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
@ -43,20 +53,9 @@ class ExtensionsFragment :
|
||||
val adapter = ExtensionAdapter()
|
||||
adapter.onItemClick = {
|
||||
activeExtension = it
|
||||
Log.d(it.id, it.name)
|
||||
if (it.isConnected()) {
|
||||
Log.d(it.id, "Continue!")
|
||||
findNavController().navigate(
|
||||
ExtensionsFragmentDirections.actionNavExtensionsToNavExtension(
|
||||
it.id
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val ls = it.connect()
|
||||
ls.observe(viewLifecycleOwner) { st ->
|
||||
Log.d("States", st.name)
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "${it.id}: ${it.name}")
|
||||
|
||||
this.extensionPermissionsVerified(it)
|
||||
}
|
||||
recycler.adapter = adapter
|
||||
|
||||
@ -71,56 +70,43 @@ class ExtensionsFragment :
|
||||
adapter.set(list)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
activeExtension.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when {
|
||||
grantResults.isEmpty() -> {
|
||||
// If user interaction was interrupted, the permission request
|
||||
// is cancelled and you receive empty arrays.
|
||||
Log.i(TAG, "User interaction was cancelled.")
|
||||
private fun extensionPermissionsVerified(it: Extension) {
|
||||
// Check for the extension permissions
|
||||
if (it.permissions != null && !PermissionsManager.hasPermission(requireContext(), it.permissions!!)) {
|
||||
// TODO: show popup explaining the permissions requested
|
||||
|
||||
// show permissions
|
||||
activityResult.launch(it.permissions)
|
||||
return
|
||||
}
|
||||
extensionIsConnected(it)
|
||||
}
|
||||
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED -> {
|
||||
Log.d(TAG, "Granted")
|
||||
activeExtension.onRequestPermissionResult(requestCode, permissions, grantResults)
|
||||
private fun extensionIsConnected(it: Extension) {
|
||||
// check if it is connected
|
||||
if (it.isConnected()) {
|
||||
gotoExtension(it)
|
||||
return
|
||||
}
|
||||
else -> {
|
||||
// Permission denied.
|
||||
|
||||
// In this Activity we've chosen to notify the user that they
|
||||
// have rejected a core permission for the app since it makes the Activity useless.
|
||||
// We're communicating this message in a Snackbar since this is a sample app, but
|
||||
// core permissions would typically be best requested during a welcome-screen flow.
|
||||
// IDK: maybe give less liberty to the extension IDK
|
||||
val ld = it.connect()
|
||||
ld.observe(viewLifecycleOwner) { state ->
|
||||
Log.d(TAG, state.toString())
|
||||
if (state == Extension.States.DONE) {
|
||||
gotoExtension(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally, it is important to remember that a permission might have been
|
||||
// rejected without asking the user for permission (device policy or "Never ask
|
||||
// again" prompts). Therefore, a user interface affordance is typically implemented
|
||||
// when permissions are denied. Otherwise, your app could appear unresponsive to
|
||||
// touches or interactions which have required permissions.
|
||||
Log.e(TAG, "Error")
|
||||
// Snackbar.make(
|
||||
// findViewById(R.id.main_activity_view),
|
||||
// R.string.permission_denied_explanation,
|
||||
// Snackbar.LENGTH_INDEFINITE)
|
||||
// .setAction(R.string.settings) {
|
||||
// // Build intent that displays the App settings screen.
|
||||
// val intent = Intent()
|
||||
// intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
// val uri = Uri.fromParts("package",
|
||||
// BuildConfig.APPLICATION_ID, null)
|
||||
// intent.data = uri
|
||||
// intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
// startActivity(intent)
|
||||
// }
|
||||
// .show()
|
||||
}
|
||||
}
|
||||
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.RectF
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -24,9 +23,9 @@ import com.dzeio.openhealth.utils.DrawUtils
|
||||
import com.dzeio.openhealth.utils.GraphUtils
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.min
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewModel::class.java) {
|
||||
@ -204,7 +203,7 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
|
||||
animator.addUpdateListener {
|
||||
|
||||
this.oldValue = 100 * it.animatedValue as Int / viewModel.dailyWaterIntake.toFloat()
|
||||
Log.d("Test2", "${this.oldValue}")
|
||||
// Log.d("Test2", "${this.oldValue}")
|
||||
|
||||
DrawUtils.drawArc(
|
||||
canvas,
|
||||
|
@ -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.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -12,12 +11,11 @@ import androidx.navigation.NavDeepLinkBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import com.dzeio.openhealth.Application
|
||||
import com.dzeio.openhealth.MainActivity
|
||||
import com.dzeio.openhealth.R
|
||||
import com.dzeio.openhealth.core.BaseService
|
||||
import com.dzeio.openhealth.interfaces.NotificationChannels
|
||||
import com.dzeio.openhealth.interfaces.NotificationIds
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WaterReminderService(
|
@ -14,7 +14,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:context=".MainActivity">
|
||||
tools:context=".ui.MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
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
|
||||
android:id="@+id/action_nav_browse_to_nav_list_weight"
|
||||
app:destination="@id/nav_list_weight" />
|
||||
<action
|
||||
android:id="@+id/action_nav_browse_to_stepsHomeFragment"
|
||||
app:destination="@id/stepsHomeFragment" />
|
||||
</fragment>
|
||||
|
||||
|
||||
@ -176,4 +179,9 @@
|
||||
android:name="com.dzeio.openhealth.ui.activity.ActivityFragment"
|
||||
android:label="@string/menu_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>
|
||||
|
@ -37,5 +37,7 @@
|
||||
<string name="menu_activity">Activity</string>
|
||||
<string name="add_goal">Ajouter un 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>
|
||||
|
@ -48,4 +48,7 @@
|
||||
|
||||
<string name="add_goal">Add 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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user