1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-06-13 09:29:19 +00:00
This commit is contained in:
2021-12-22 17:36:12 +01:00
parent 077397749c
commit 8c33478e17
16 changed files with 318 additions and 105 deletions

View File

@ -42,7 +42,6 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'javax.inject:javax.inject:1'
@ -50,8 +49,18 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
// DataStore
implementation "androidx.datastore:datastore:1.0.0"
// Navigation
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
// Services
implementation 'androidx.work:work-runtime-ktx:2.7.1'
// Tests
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@ -71,7 +80,7 @@ dependencies {
implementation files('libs/samsung-health-data-1.5.0.aar')
// ROOM
def room_version = "2.3.0"
def room_version = "2.4.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

View File

@ -23,6 +23,19 @@
android:supportsRtl="true"
android:theme="@style/Theme.OpenHealth">
<receiver android:name=".services.BroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<!-- Water Intake -->
<service android:name=".services.WaterReminderService"
android:label="Water Intake service"
android:permission="android.permission.BIND_JOB_SERVICE" />
<!-- Samsung Health-->
<meta-data
android:name="com.samsung.android.health.permission.read"

View File

@ -4,4 +4,8 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class Application : Application() {}
class Application : Application() {
companion object {
const val TAG = "OpenHealth"
}
}

View File

@ -1,6 +1,10 @@
package com.dzeio.openhealth
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -13,6 +17,7 @@ import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.dzeio.openhealth.core.BaseActivity
import com.dzeio.openhealth.databinding.ActivityMainBinding
import com.dzeio.openhealth.services.WaterReminderService
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@ -41,6 +46,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
setupActionBarWithNavController(navController, appBarConfiguration)
WaterReminderService.setup(this)
createNotificationChannel()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -66,8 +74,23 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("MainActivity", "onActivityResult $requestCode $resultCode")
for (fragment in supportFragmentManager.primaryNavigationFragment!!.childFragmentManager.fragments) {
fragment.onActivityResult(requestCode, resultCode, data)
}
}
private fun createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("openhealth", "Default Channel", importance)
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}

View File

@ -0,0 +1,25 @@
package com.dzeio.openhealth.core
import android.content.Context
import android.util.Log
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)
Log.d("OpenHealth/BaseService", "Scheduled Job $tag")
WorkManager.getInstance(context)
.enqueue(request)
}
}
}

View File

@ -26,4 +26,13 @@ data class Water(
}
fun formatTimestamp(): String = getDateInstance().format(Date(timestamp))
fun isToday(): Boolean {
val cal = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
return timestamp == cal.timeInMillis
}
}

View File

@ -21,12 +21,6 @@ class WaterRepository @Inject constructor(
suspend fun deleteFromSource(value: String) = waterDao.deleteFromSource(value)
fun todayWater() = lastWater().filter {
val cal = Calendar.getInstance()
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
Log.d("WaterRepository", "${it?.timestamp} ${cal.timeInMillis}")
return@filter it?.timestamp == cal.timeInMillis
return@filter it != null && it.isToday()
}
}

View File

@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface WeightDao : BaseDao<Weight> {
@Query("SELECT * FROM Weight")
@Query("SELECT * FROM Weight ORDER BY timestamp")
fun getAll(): Flow<List<Weight>>
@Query("SELECT * FROM Weight where id = :weightId")

View File

@ -0,0 +1,18 @@
package com.dzeio.openhealth.services
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
class BroadcastReceiver : BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context?, intent: Intent?) {
context?.let {
WaterReminderService.setup(it)
Log.d("OpenHealth/BR", "Scheduled Jobs!")
}
}
}

View File

@ -0,0 +1,86 @@
package com.dzeio.openhealth.services
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
import androidx.core.app.NotificationManagerCompat
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 java.util.*
import java.util.concurrent.TimeUnit
class WaterReminderService(
private val context: Context,
params: WorkerParameters
) : BaseService(context, params) {
companion object {
const val TAG = "${Application.TAG}/WaterService"
fun setup(context: Context) {
schedule(
TAG,
PeriodicWorkRequestBuilder<WaterReminderService>(15, TimeUnit.MINUTES)
.addTag(TAG)
.build(),
context
)
}
}
@SuppressLint("UnspecifiedImmutableFlag")
override fun doWork(): Result {
Log.d(TAG, "Ran! ${Date().toLocaleString()}")
with(NotificationManagerCompat.from(context)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notify(
(Math.random() * 100).toInt(), NotificationCompat.Builder(context, "openhealth")
.setContentTitle("Did you drink?")
.setContentText("Drink now!")
.setSmallIcon(R.drawable.ic_logo_small)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
},
PendingIntent.FLAG_IMMUTABLE
)
)
.build()
)
} else {
notify(
(Math.random() * 100).toInt(), NotificationCompat.Builder(context, "openhealth")
.setContentTitle("Did you drink?")
.setContentText("Drink now!")
.setSmallIcon(R.drawable.ic_logo_small)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(
PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
},
0
)
)
.build()
)
}
}
return Result.success()
}
}

View File

@ -1,20 +1,14 @@
package com.dzeio.openhealth.ui.home
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.content.pm.PackageManager
import android.animation.ValueAnimator
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.Rect
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.dzeio.openhealth.R
@ -36,10 +30,10 @@ import com.google.android.material.color.MaterialColors
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.io.IOException
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.min
@AndroidEntryPoint
class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewModel::class.java) {
@ -56,38 +50,39 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.init()
binding.addWeight.setOnClickListener {
AddWeightDialog().show(requireActivity().supportFragmentManager, null)
}
binding.fragmentHomeWaterAdd.setOnClickListener {
lifecycleScope.launchWhenStarted {
binding.fragmentHomeWaterAdd.setOnClickListener { _ ->
Log.d(TAG, "Collecting latest $this")
if (viewModel.fetchTodayWater().count() == 0) {
Log.d(TAG, "No value, Adding...")
val w = Water()
w.value = 200
viewModel.updateWater(w)
return@launchWhenStarted
val water = viewModel.water.value
if (water == null) {
val w = Water()
w.value = 200
viewModel.updateWater(w)
} else {
water.value += 200
viewModel.updateWater(water)
}
}
binding.fragmentHomeWaterRemove.setOnClickListener { _ ->
val water = viewModel.water.value
if (water != null) {
water.value -= 200
if (water.value == 0) {
viewModel.deleteWater(water)
} else {
viewModel.updateWater(water)
}
try {
viewModel.fetchTodayWater().count()
val item = viewModel.fetchTodayWater().lastOrNull()
Log.d(TAG, "Collected latest $item")
if (item == null) {
val w = Water()
w.value = 200
viewModel.updateWater(w)
} else {
item.value += 200
viewModel.updateWater(item)
}
} catch (e: IOException) {
Log.e(TAG, "EXCEPTION", e)
}
}
}
@ -120,13 +115,26 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
}
// Setup Graph
// binding.weightGraph.gridLabelRenderer.labelFormatter = DateAsXAxisLabelFormatter(requireContext())
// binding.weightGraph.gridLabelRenderer.numHorizontalLabels = 3 // only 4 because of the space
// binding.weightGraph.viewport.isXAxisBoundsManual = true
//// binding.weightGraph.gridLabelRenderer.setHumanRounding(false);
// binding.weightGraph.addSeries(serie)
// binding.weightGraph.apply {
// gridLabelRenderer.labelFormatter = DateAsXAxisLabelFormatter(
// requireContext(),
// DateFormat.getDateInstance(DateFormat.MEDIUM)
// )
// gridLabelRenderer.numHorizontalLabels = 3 // only 4 because of the space
// gridLabelRenderer.numVerticalLabels = 3
// gridLabelRenderer.gridStyle = GridLabelRenderer.GridStyle.HORIZONTAL
// viewport.isXAxisBoundsManual = true
// viewport.isScalable = true
// viewport.setScalableY(true)
// viewport.isScrollable = true
// viewport.setScrollableY(true)
// gridLabelRenderer.setHumanRounding(false);
// addSeries(serie)
// }
}
// private val serie: LineGraphSeries<DataPoint> = LineGraphSeries()
// private val entries = LineGraphSeries<DataPoint>()
private fun updateGraph(list: List<Weight>) {
@ -173,37 +181,41 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
}
binding.weightGraph.data = LineData(dataSet)
binding.weightGraph.invalidate()
binding.weightGraph.setVisibleXRange(
(list[list.size - 1].timestamp - 604800000).toFloat(),
list[list.size - 1].timestamp.toFloat()
)
}
// binding.weightGraph.gridLabelRenderer.numHorizontalLabels = 3 // only 4 because of the space
// binding.weightGraph.gridLabelRenderer.numVerticalLabels = 3
//
// binding.weightGraph.viewport.maxXAxisSize = 604800000.0 * 2
//// binding.weightGraph.viewport.setMinimalViewport(0.0, 604800000.0, minValue.toDouble(), maxValue.toDouble())
//
// serie.resetData(list
// .map { DataPoint(Date(it.timestamp), it.weight.toDouble()) }
// .toTypedArray()
// )
// .toTypedArray())
//
// binding.weightGraph.viewport.scrollToEnd()
}
@SuppressLint("PrivateResource")
override fun onStart() {
super.onStart()
lifecycleScope.launchWhenStarted {
viewModel.fetchWeights().collectLatest {
if (it.isEmpty()) {
return@collectLatest
}
updateGraph(it)
}
}
lifecycleScope.launchWhenStarted {
viewModel.fetchTodayWater().collect {
Log.d(TAG, "Pouet? $it")
if (it != null) {
updateWater(it.value)
} else {
updateWater(0)
}
viewModel.water.observe(viewLifecycleOwner) {
if (it != null) {
updateWater(it.value)
} else {
updateWater(0)
}
}
@ -212,7 +224,8 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
}
private fun updateWater(water: Int) {
binding.fragmentHomeWaterCurrent.text = water.toString()
val oldValue = binding.fragmentHomeWaterCurrent.text.toString().replace("ml", "").toInt()
binding.fragmentHomeWaterCurrent.text = "${water}ml"
val graph = BitmapUtils.convertToMutable(
requireContext(),
@ -220,22 +233,24 @@ class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(HomeViewMo
)
graph?.let { btmp ->
val canvas = Canvas(btmp)
DrawUtils.drawArc(
canvas,
100 * water / 1200,
MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorPrimary
)
)
canvas.save()
// val params = binding.background.layoutParams
// params.height = binding.background.measuredWidth
// binding.background.layoutParams = params
binding.background.setImageBitmap(graph)
ValueAnimator.ofFloat(min(oldValue.toFloat(), 1200f), min(water.toFloat(), 1200f))
.apply {
duration = 300
addUpdateListener {
val canvas = Canvas(btmp)
DrawUtils.drawArc(
canvas,
100 * it.animatedValue as Float / 1200f,
MaterialColors.getColor(
requireView(),
com.google.android.material.R.attr.colorPrimary
)
)
canvas.save()
binding.background.setImageBitmap(graph)
}
start()
}
}
}
}

View File

@ -1,5 +1,8 @@
package com.dzeio.openhealth.ui.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.dzeio.openhealth.core.BaseViewModel
import com.dzeio.openhealth.data.weight.Water
import com.dzeio.openhealth.data.weight.WaterRepository
@ -9,6 +12,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
@ -29,8 +33,25 @@ class HomeViewModel @Inject internal constructor(
fun fetchTodayWater() = waterRepository.todayWater()
suspend fun updateWater(water: Water) = waterRepository.addWater(water)
suspend fun deleteWater(water: Water) = waterRepository.deleteWater(water)
val water: MutableLiveData<Water?> = MutableLiveData(null)
fun init() {
viewModelScope.launch {
waterRepository.todayWater().collectLatest {
water.postValue(it)
}
}
}
fun updateWater(water: Water) {
viewModelScope.launch {
waterRepository.addWater(water)
}
}
fun deleteWater(item: Water) {
viewModelScope.launch {
waterRepository.deleteWater(item)
water.postValue(null)
}
}
}

View File

@ -7,7 +7,7 @@ object DrawUtils {
/**
* Fuck Graphics
*/
fun drawArc(canvas: Canvas, percent: Int, pColor: Int) {
fun drawArc(canvas: Canvas, percent: Float, pColor: Int) {
canvas.width
val spacing = 120f
val r1 = RectF(

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="324dp"
android:height="324dp"
android:viewportWidth="324"
android:viewportHeight="324">
<path
android:pathData="M290,199.975C302.15,199.975 312,190.125 312,177.975V146.025C312,133.875 302.15,124.025 290,124.025H212.975C205.795,124.025 199.975,118.205 199.975,111.025V34C199.975,21.85 190.125,12 177.975,12H146.025C133.875,12 124.025,21.85 124.025,34V111.025C124.025,118.205 118.205,124.025 111.025,124.025H34C21.85,124.025 12,133.875 12,146.025V177.975C12,190.125 21.85,199.975 34,199.975H111.025C118.205,199.975 124.025,205.795 124.025,212.975V290C124.025,302.15 133.875,312 146.025,312H177.975C190.125,312 199.975,302.15 199.975,290V212.975C199.975,205.795 205.795,199.975 212.975,199.975H290Z"
android:strokeWidth="23"
android:fillColor="#ffffff"
android:fillAlpha="0.5"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>