Commit e0ccb4f5 authored by Joey's avatar Joey

app: merge overview and glucose fragments into one

Signed-off-by: Joey's avatarJoey <bevilacquajoey@gmail.com>
Change-Id: Ib4866d3e62db7ac83401c461d9711802edabd860
parent e887646c
......@@ -6,22 +6,44 @@
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.viewmodels.glucose
package it.diab.viewmodels
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import it.diab.data.entities.TimeFrame
import it.diab.data.extensions.glucose
import it.diab.data.extensions.insulin
import it.diab.data.repositories.GlucoseRepository
import it.diab.data.repositories.InsulinRepository
import it.diab.ui.models.DataSetsModel
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class GlucoseListViewModelTest {
private lateinit var viewModel: GlucoseListViewModel
class MainViewModelTest {
private lateinit var viewModel: MainViewModel
private val testTimeFrame = TimeFrame.MORNING
private val glucoseValues = arrayOf(69, 99, 301, 132)
private val glucoseArray = Array(4) { i ->
glucose {
value = glucoseValues[i]
timeFrame = testTimeFrame
}
}
private val insulinArray = arrayOf(
insulin {
uid = 73
name = "Foo"
},
insulin {
uid = 42
name = "Oof"
}
)
@get:Rule
val rule = InstantTaskExecutorRule()
......@@ -33,11 +55,11 @@ class GlucoseListViewModelTest {
// Insert test data
InsulinRepository.getInstance(context).apply {
setDebugMode()
insert(TEST_DATA[0])
insert(TEST_DATA[1])
insert(insulinArray[0])
insert(insulinArray[1])
}
viewModel = GlucoseListViewModel(
viewModel = MainViewModel(
GlucoseRepository.getInstance(context),
InsulinRepository.getInstance(context)
).apply {
......@@ -47,21 +69,20 @@ class GlucoseListViewModelTest {
@Test
fun getInsulin() {
val test = TEST_DATA[0]
val test = insulinArray[0]
assertEquals(test, viewModel.getInsulin(test.uid))
Assert.assertEquals(test, viewModel.getInsulin(test.uid))
}
companion object {
private val TEST_DATA = arrayOf(
insulin {
uid = 73
name = "Foo"
},
insulin {
uid = 42
name = "Oof"
}
@Test
fun getAverageLastWeek() = runBlocking {
val model = viewModel.runGetDataSets(glucoseArray.asList())
Assert.assertTrue(model is DataSetsModel.Available)
model as DataSetsModel.Available
Assert.assertEquals(
glucoseValues.average().toFloat(),
model.average[testTimeFrame.toInt()].y
)
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.viewmodels.glucose
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import it.diab.data.entities.TimeFrame
import it.diab.data.repositories.GlucoseRepository
import it.diab.data.extensions.glucose
import it.diab.viewmodels.overview.OverviewViewModel
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class OverviewViewModelTest {
private lateinit var viewModel: OverviewViewModel
private val testTimeFrame = TimeFrame.MORNING
private val glucoseValues = arrayOf(69, 99, 301, 132)
private val glucoseList = Array(4) { i ->
glucose {
value = glucoseValues[i]
timeFrame = testTimeFrame
}
}
@get:Rule
val rule = InstantTaskExecutorRule()
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
val repo = GlucoseRepository.getInstance(context).apply {
setDebugMode()
}
viewModel = OverviewViewModel(repo)
}
@Test
fun getAverageLastWeek() = runBlocking {
val pair = viewModel.runGetDataSets(glucoseList.asList())
assertEquals(glucoseValues.average().toFloat(), pair.second[testTimeFrame.toInt()].y)
}
}
\ No newline at end of file
......@@ -10,83 +10,18 @@ package it.diab
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.viewpager.widget.ViewPager
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.tabs.TabLayout
import it.diab.adapters.FragmentsPagerAdapter
import it.diab.core.util.Activities
import it.diab.core.util.event.EventObserver
import it.diab.core.util.intentTo
import it.diab.fragments.GlucoseListFragment
import it.diab.fragments.InsulinFragment
import it.diab.fragments.OverviewFragment
import it.diab.util.ShortcutUtils
class MainActivity : AppCompatActivity() {
private lateinit var fab: FloatingActionButton
private val fragmentsLifeCycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(
fm: FragmentManager,
f: Fragment,
v: View,
savedInstanceState: Bundle?
) {
super.onFragmentViewCreated(fm, f, v, savedInstanceState)
// Re-bind click listener when glucose fragment is re-created
if (f is GlucoseListFragment) {
f.openGlucose.observe(
this@MainActivity,
EventObserver(this@MainActivity::onGlucoseClick)
)
}
}
}
public override fun onCreate(savedInstance: Bundle?) {
super.onCreate(savedInstance)
setContentView(R.layout.activity_main)
val tabLayout = findViewById<TabLayout>(R.id.tabs)
val viewPager = findViewById<ViewPager>(R.id.viewpager)
fab = findViewById(R.id.fab)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentsLifeCycleCallback, false)
viewPager.adapter = FragmentsPagerAdapter(
supportFragmentManager,
resources,
OverviewFragment(),
GlucoseListFragment(),
InsulinFragment()
)
tabLayout.setupWithViewPager(viewPager)
fab.setOnClickListener { onGlucoseClick(-1) }
setContentView(R.layout.activity_main)
createShortcuts()
}
override fun onDestroy() {
supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentsLifeCycleCallback)
super.onDestroy()
}
private fun onGlucoseClick(uid: Long) {
val intent = intentTo(Activities.Glucose.Editor).apply {
putExtra(Activities.Glucose.Editor.EXTRA_UID, uid)
}
val optionsCompat = ActivityOptionsCompat
.makeSceneTransitionAnimation(this, fab, fab.transitionName)
startActivity(intent, optionsCompat.toBundle())
}
private fun createShortcuts() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ShortcutUtils.setupShortcuts(this)
......
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.adapters
import android.content.res.Resources
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import it.diab.fragments.BaseFragment
class FragmentsPagerAdapter(
manager: FragmentManager,
private val resources: Resources,
vararg fragment: BaseFragment
) : FragmentPagerAdapter(manager) {
private val fragments: Array<out BaseFragment> = fragment
override fun getCount() = fragments.size
override fun getItem(position: Int) = fragments[position]
override fun getPageTitle(position: Int): String = fragments[position].getTitle(resources)
}
\ No newline at end of file
......@@ -22,18 +22,22 @@ import it.diab.R
import it.diab.core.util.PreferencesUtil
import it.diab.core.util.event.Event
import it.diab.data.entities.Glucose
import it.diab.data.entities.Insulin
import it.diab.holders.GlucoseHolder
import it.diab.holders.GlucoseHolderCallbacks
import it.diab.holders.HeaderHolder
import it.diab.holders.MainHolder
import it.diab.ui.models.DataSetsModel
import it.diab.ui.models.LastGlucoseModel
import it.diab.util.UIUtils
import it.diab.viewmodels.glucose.GlucoseListViewModel
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class GlucoseListAdapter(
val context: Context,
private val viewModel: GlucoseListViewModel
) : PagedListAdapter<Glucose, GlucoseHolder>(CALLBACK), GlucoseHolderCallbacks {
class MainAdapter(
private val context: Context,
private val callbacks: Callbacks
) : PagedListAdapter<Glucose, MainHolder>(CALLBACK), GlucoseHolderCallbacks {
private val _openGlucose = MutableLiveData<Event<Long>>()
val openGlucose: LiveData<Event<Long>> = _openGlucose
......@@ -47,12 +51,33 @@ class GlucoseListAdapter(
private val hourFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GlucoseHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_glucose, parent, false),
this
if (viewType == VIEW_HEADER) {
HeaderHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_header, parent, false)
)
} else {
GlucoseHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_glucose, parent, false),
this
)
}
override fun onBindViewHolder(holder: MainHolder, position: Int) {
if (holder is HeaderHolder) {
bindHeader(holder)
} else if (holder is GlucoseHolder) {
bindGlucose(holder, position - 1)
}
}
private fun bindHeader(holder: HeaderHolder) {
holder.bind(
callbacks.getLastGlucose(),
callbacks.getDataSets()
)
}
override fun onBindViewHolder(holder: GlucoseHolder, position: Int) {
private fun bindGlucose(holder: GlucoseHolder, position: Int) {
val item = getItem(position)
if (item == null) {
holder.onLoading()
......@@ -71,13 +96,19 @@ class GlucoseListAdapter(
else -> null
}
override fun getInsulinName(uid: Long) =
viewModel.getInsulin(uid).name
override fun getInsulinName(uid: Long) = callbacks.getInsulin(uid).name
override fun onClick(uid: Long) {
_openGlucose.value = Event(uid)
}
override fun getItemViewType(position: Int) = if (position == 0) VIEW_HEADER else VIEW_GLUCOSE
override fun getItemCount(): Int {
// First item is for the header
return super.getItemCount() + 1
}
private fun buildIndicator(@ColorRes colorId: Int): Drawable? {
val resources = context.resources
val color = ContextCompat.getColor(context, colorId)
......@@ -85,6 +116,12 @@ class GlucoseListAdapter(
return UIUtils.createRoundDrawable(resources, size, color)
}
interface Callbacks {
fun getInsulin(uid: Long): Insulin
fun getLastGlucose(): LastGlucoseModel
fun getDataSets(): DataSetsModel
}
companion object {
private val CALLBACK = object : DiffUtil.ItemCallback<Glucose>() {
override fun areContentsTheSame(oldItem: Glucose, newItem: Glucose) =
......@@ -93,5 +130,8 @@ class GlucoseListAdapter(
override fun areItemsTheSame(oldItem: Glucose, newItem: Glucose) =
oldItem.uid == newItem.uid
}
private const val VIEW_HEADER = 0
private const val VIEW_GLUCOSE = 1
}
}
......@@ -12,46 +12,50 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.core.app.ActivityOptionsCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.paging.PagedList
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import it.diab.R
import it.diab.adapters.GlucoseListAdapter
import it.diab.core.util.event.Event
import it.diab.adapters.MainAdapter
import it.diab.core.util.Activities
import it.diab.core.util.event.EventObserver
import it.diab.core.util.intentTo
import it.diab.data.entities.Glucose
import it.diab.data.repositories.GlucoseRepository
import it.diab.data.repositories.InsulinRepository
import it.diab.ui.TimeHeaderDecoration
import it.diab.ui.models.DataSetsModel
import it.diab.ui.models.LastGlucoseModel
import it.diab.ui.widgets.RecyclerViewExt
import it.diab.util.extensions.doOnNextLayout
import it.diab.util.extensions.removeAllItemDecorators
import it.diab.viewmodels.glucose.GlucoseListViewModel
import it.diab.viewmodels.glucose.GlucoseListViewModelFactory
import it.diab.viewmodels.MainViewModel
import it.diab.viewmodels.MainViewModelFactory
class GlucoseListFragment : BaseFragment() {
override val titleRes = R.string.fragment_glucose
class MainFragment : Fragment(), MainAdapter.Callbacks {
private lateinit var recyclerView: RecyclerView
private lateinit var viewModel: MainViewModel
private lateinit var listAdapter: MainAdapter
private lateinit var viewModel: GlucoseListViewModel
private lateinit var adapter: GlucoseListAdapter
private lateinit var fab: FloatingActionButton
private lateinit var glucoseList: RecyclerViewExt
private val _openGlucose = MutableLiveData<Event<Long>>()
val openGlucose: LiveData<Event<Long>> = _openGlucose
private var last: LastGlucoseModel = LastGlucoseModel.Loading
private var dataSetsModel: DataSetsModel = DataSetsModel.Loading
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = context ?: return
val factory = GlucoseListViewModelFactory(
val factory = MainViewModelFactory(
GlucoseRepository.getInstance(context),
InsulinRepository.getInstance(context)
)
viewModel = ViewModelProviders.of(this, factory)[GlucoseListViewModel::class.java]
viewModel = ViewModelProviders.of(this, factory)[MainViewModel::class.java]
}
override fun onCreateView(
......@@ -59,43 +63,87 @@ class GlucoseListFragment : BaseFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.fragment_glucose, container, false)
recyclerView = view.findViewById(R.id.glucose_recyclerview)
val view = inflater.inflate(R.layout.fragment_main, container, false)
fab = view.findViewById(R.id.fab)
glucoseList = view.findViewById(R.id.main_list)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = activity ?: return
val context = context ?: return
listAdapter = MainAdapter(context, this)
fab.setOnClickListener { onFabClick() }
viewModel.prepare {
adapter = GlucoseListAdapter(activity, viewModel)
val activity = activity ?: return@prepare
viewModel.pagedList.observe(activity, Observer(this::onPagedListChanged))
viewModel.liveList.observe(activity, Observer(this::onLiveListChanged))
listAdapter.openGlucose.observe(activity, EventObserver(this::onItemClick))
}
}
recyclerView.adapter = adapter
override fun getInsulin(uid: Long) = viewModel.getInsulin(uid)
viewModel.pagedList.observe(activity, Observer(this::update))
viewModel.liveList.observe(activity, Observer(this::updateHeaders))
adapter.openGlucose.observe(activity, EventObserver(this::onItemClick))
private fun onItemClick(uid: Long) {
val activity = activity ?: return
val intent = intentTo(Activities.Glucose.Editor).apply {
putExtra(Activities.Glucose.Editor.EXTRA_UID, uid)
}
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
fab,
fab.transitionName
)
startActivity(intent, options.toBundle())
}
private fun update(data: PagedList<Glucose>?) {
adapter.submitList(data)
private fun onFabClick() {
onItemClick(-1)
}
private fun onItemClick(uid: Long) {
_openGlucose.value = Event(uid)
private fun onPagedListChanged(data: PagedList<Glucose>?) {
listAdapter.submitList(data)
if (glucoseList.adapter == null) {
glucoseList.adapter = listAdapter
}
}
private fun updateHeaders(list: List<Glucose>) {
if (list.isEmpty()) {
private fun onLiveListChanged(data: List<Glucose>?) {
if (data == null) {
return
}
recyclerView.doOnNextLayout {
recyclerView.removeAllItemDecorators()
recyclerView.addItemDecoration(TimeHeaderDecoration(requireContext(), list))
if (data.isEmpty()) {
last = LastGlucoseModel.Empty
dataSetsModel = DataSetsModel.Empty
} else {
last = LastGlucoseModel.Available(data[0])
}
updateHeaders(data)
viewModel.getDataSets(this::onGraphDataSetChanged)
}
override fun getLastGlucose() = last
override fun getDataSets() = dataSetsModel
private fun onGraphDataSetChanged(model: DataSetsModel) {
dataSetsModel = model
listAdapter.notifyItemChanged(0)
}
private fun updateHeaders(list: List<Glucose>) {
glucoseList.doOnNextLayout {
glucoseList.removeAllItemDecorators()
glucoseList.addItemDecoration(TimeHeaderDecoration(requireContext(), list, 1))
}
}
}
\ No newline at end of file
/*
* Copyright (c) 2019 Bevilacqua Joey
*
* Licensed under the GNU GPLv3 license
*
* The text of the license can be found in the LICENSE file
* or at https://www.gnu.org/licenses/gpl.txt
*/
package it.diab.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import it.diab.R
import it.diab.core.override.BaseFitHandler
import it.diab.core.util.Activities
import it.diab.core.util.SystemUtil
import it.diab.core.util.intentTo
import it.diab.data.entities.Glucose
import it.diab.data.repositories.GlucoseRepository
import it.diab.ui.graph.OverviewGraphView
import it.diab.ui.graph.OverviewValueFormatter
import it.diab.util.extensions.isToday
import it.diab.viewmodels.overview.OverviewViewModel
import it.diab.viewmodels.overview.OverviewViewModelFactory
import java.text.SimpleDateFormat
import java.util.Locale
class OverviewFragment : BaseFragment() {
override val titleRes = R.string.fragment_overview
private lateinit var lastValueView: TextView
private lateinit var lastDescView: TextView
private lateinit var chart: OverviewGraphView
private lateinit var menuView: ImageView
private lateinit var viewModel: OverviewViewModel
private val hourFormatter = SimpleDateFormat(" (HH:mm)", Locale.getDefault())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = context ?: return
val factory = OverviewViewModelFactory(GlucoseRepository.getInstance(context))
viewModel = ViewModelProviders.of(this, factory)[OverviewViewModel::class.java]
viewModel.prepare(
SystemUtil.getOverrideObject(
BaseFitHandler::class.java, context,
R.string.config_class_fit_handler
)
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.fragment_overview, container, false)
lastValueView = view.findViewById(R.id.overview_last_value)
lastDescView = view.findViewById(R.id.overview_last_desc)
chart = view.findViewById(R.id.overview_chart)
menuView = view.findViewById(R.id.overview_menu)
viewModel.last.observe(this, Observer(this::updateLast))
viewModel.list.observe(this, Observer(this::updateChart))
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupMenu()
}
private fun updateChart(data: List<Glucose>?) {
if (data == null || data.isEmpty()) {
return
}
viewModel.getDataSets(this::setDataSets)
}
private fun updateLast(data: List<Glucose>?) {
if (data == null) {
return
}
if (data.isEmpty()) {
lastValueView.text = getString(R.string.overview_last_fallback)
lastDescView.text = getString(R.string.overview_last_desc_fallback)
return
}
val glucose = data[0]
lastValueView.text = "${glucose.value}"
lastDescView.text = getString(
R.string.overview_last_desc,
if (glucose.date.isToday()) hourFormatter.format(glucose.date)
else ""
)
}
@SuppressLint("RestrictedApi") // Needed for MenuPopupHelper
private fun setupMenu() {
val context = context ?: return
val ctxWrapper = ContextThemeWrapper(context, R.style.AppTheme_PopupMenuOverlapAnchor)
val popupMenu = PopupMenu(
ctxWrapper, menuView, Gravity.NO_GRAVITY,