Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

CheckIn cards & interaction (EXPOSUREAPP-5410) #2644

Merged
merged 8 commits into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins

import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.PastCheckInVH
import de.rki.coronawarnapp.util.lists.BindableVH
import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter
import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffer
import de.rki.coronawarnapp.util.lists.modular.ModularAdapter
import de.rki.coronawarnapp.util.lists.modular.mods.DataBinderMod
import de.rki.coronawarnapp.util.lists.modular.mods.StableIdMod
import de.rki.coronawarnapp.util.lists.modular.mods.TypedVHCreatorMod

class CheckInsAdapter :
ModularAdapter<CheckInsAdapter.ItemVH<CheckInsItem, ViewBinding>>(),
AsyncDiffUtilAdapter<CheckInsItem> {

override val asyncDiffer: AsyncDiffer<CheckInsItem> = AsyncDiffer(adapter = this)

init {
modules.addAll(
listOf(
StableIdMod(data),
DataBinderMod<CheckInsItem, ItemVH<CheckInsItem, ViewBinding>>(data),
TypedVHCreatorMod({ data[it] is ActiveCheckInVH.Item }) { ActiveCheckInVH(it) },
TypedVHCreatorMod({ data[it] is PastCheckInVH.Item }) { PastCheckInVH(it) },
)
)
}

override fun getItemCount(): Int = data.size

abstract class ItemVH<Item : CheckInsItem, VB : ViewBinding>(
@LayoutRes layoutRes: Int,
parent: ViewGroup
) : ModularAdapter.VH(layoutRes, parent), BindableVH<Item, VB>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.net.toUri
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import androidx.navigation.NavOptions
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.DefaultItemAnimator
import com.google.android.material.transition.Hold
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsFragmentBinding
import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator
import de.rki.coronawarnapp.util.lists.diffutil.update
import de.rki.coronawarnapp.util.ui.doNavigate
import de.rki.coronawarnapp.util.ui.observe2
import de.rki.coronawarnapp.util.ui.viewBindingLazy
Expand All @@ -35,6 +40,7 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
}
)
private val binding: TraceLocationAttendeeCheckinsFragmentBinding by viewBindingLazy()
private val checkInsAdapter = CheckInsAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -45,6 +51,20 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
super.onViewCreated(view, savedInstanceState)
setupMenu(binding.toolbar)

binding.checkInsList.apply {
adapter = checkInsAdapter
addItemDecoration(TopBottomPaddingDecorator(topPadding = R.dimen.spacing_tiny))
itemAnimator = DefaultItemAnimator()
}

viewModel.checkins.observe2(this) {
checkInsAdapter.update(it)
binding.apply {
checkInsList.isGone = it.isEmpty()
emptyListInfoContainer.isGone = it.isNotEmpty()
}
}

binding.scanCheckinQrcodeFab.apply {
setOnClickListener {
findNavController().navigate(
Expand All @@ -56,22 +76,26 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
}
if (CWADebug.isDeviceForTestersBuild) {
setOnLongClickListener {
findNavController().navigate(createCheckInUri(DEBUG_CHECKINS.random()))
findNavController().navigate(
createCheckInUri(DEBUG_CHECKINS.random()),
NavOptions.Builder().apply {
setLaunchSingleTop(true)
}.build()
)
true
}
}
}

viewModel.verifyResult.observe2(this) {
viewModel.confirmationEvent.observe2(this) {
doNavigate(
CheckInsFragmentDirections
.actionCheckInsFragmentToConfirmCheckInFragment(it.verifiedTraceLocation)
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(it.verifiedTraceLocation)
)
}
}

private fun setupMenu(toolbar: Toolbar) = toolbar.apply {
inflateMenu(R.menu.menu_trace_location_my_check_ins)
inflateMenu(R.menu.menu_trace_location_attendee_checkins)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_information -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asLiveData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationQRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationVerifyResult
import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.reporting.report
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.PastCheckInVH
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.joda.time.Duration
import org.joda.time.Instant
import timber.log.Timber

class CheckInsViewModel @AssistedInject constructor(
Expand All @@ -24,8 +31,28 @@ class CheckInsViewModel @AssistedInject constructor(
private val qrCodeUriParser: QRCodeUriParser
) : CWAViewModel(dispatcherProvider) {

private val verifyResultData = MutableLiveData<TraceLocationVerifyResult>()
val verifyResult: LiveData<TraceLocationVerifyResult> = verifyResultData
val confirmationEvent = SingleLiveEvent<TraceLocationVerifyResult>()

val checkins = FAKE_CHECKIN_SOURCE
.map { checkins -> checkins.sortedBy { it.checkInEnd } }
Copy link
Contributor

@ralfgehrer ralfgehrer Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need 2nd level sort order - e.g. checkInStart - to have a stable sorting for active events?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, by title would make sense, will add in follow up PRs.

.map { checkins ->
checkins.map { checkin ->
when {
checkin.checkInEnd == null -> ActiveCheckInVH.Item(
checkin = checkin,
onCardClicked = { /* TODO */ },
onRemoveItem = { /* TODO */ },
onCheckout = { /* TODO */ }
)
else -> PastCheckInVH.Item(
checkin = checkin,
onCardClicked = { /* TODO */ },
onRemoveItem = { /* TODO */ }
)
}
}
}
.asLiveData(context = dispatcherProvider.Default)

init {
deepLink?.let {
Expand All @@ -47,7 +74,7 @@ class CheckInsViewModel @AssistedInject constructor(

val verifyResult = traceLocationQRCodeVerifier.verify(signedTraceLocation.toByteArray())
Timber.i("verifyResult: $verifyResult")
verifyResultData.postValue(verifyResult)
confirmationEvent.postValue(verifyResult)
} catch (e: Exception) {
Timber.d(e, "TraceLocation verification failed")
e.report(ExceptionCategory.INTERNAL)
Expand All @@ -66,3 +93,42 @@ class CheckInsViewModel @AssistedInject constructor(
): CheckInsViewModel
}
}

private val FAKE_CHECKINS = listOf(
CheckIn(
id = 1,
guid = "testGuid2",
version = 1,
type = 1,
description = "Jahrestreffen der deutschen SAP Anwendergruppe",
address = "Hauptstr. 3, 69115 Heidelberg",
traceLocationStart = null,
traceLocationEnd = null,
defaultCheckInLengthInMinutes = 3 * 60,
signature = "Signature",
checkInStart = Instant.now().minus(Duration.standardHours(2)),
checkInEnd = null,
targetCheckInEnd = Instant.now().plus(Duration.standardHours(1)),
createJournalEntry = true
),
CheckIn(
id = 2,
guid = "testGuid1",
version = 1,
type = 2,
description = "CWA Launch Party",
address = "At home! Do you want the 'rona?",
traceLocationStart = Instant.parse("2021-01-01T12:00:00.000Z"),
traceLocationEnd = Instant.parse("2021-01-01T15:00:00.000Z"),
defaultCheckInLengthInMinutes = 15,
signature = "Signature",
checkInStart = Instant.parse("2021-01-01T12:30:00.000Z"),
checkInEnd = Instant.parse("2021-01-01T14:00:00.000Z"),
targetCheckInEnd = Instant.parse("2021-01-01T12:45:00.000Z"),
createJournalEntry = true
)
)

private val FAKE_CHECKIN_SOURCE = flow {
emit(FAKE_CHECKINS)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items

import android.view.ViewGroup
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.contactdiary.util.getLocale
import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsItemActiveBinding
import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
import org.joda.time.Duration
import org.joda.time.Instant
import org.joda.time.PeriodType
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.PeriodFormat
import org.joda.time.format.PeriodFormatterBuilder

class ActiveCheckInVH(parent: ViewGroup) :
BaseCheckInVH<ActiveCheckInVH.Item, TraceLocationAttendeeCheckinsItemActiveBinding>(
layoutRes = R.layout.trace_location_attendee_checkins_item_active,
parent = parent
) {

override val viewBinding: Lazy<TraceLocationAttendeeCheckinsItemActiveBinding> = lazy {
TraceLocationAttendeeCheckinsItemActiveBinding.bind(itemView)
}

private val hourPeriodFormatter by lazy {
PeriodFormat.wordBased(context.getLocale())
}

override val onBindData: TraceLocationAttendeeCheckinsItemActiveBinding.(
item: Item,
payloads: List<Any>
) -> Unit = { item, _ ->
val checkInStartUserTZ = item.checkin.checkInStart.toUserTimeZone()

val checkinDuration = Duration(checkInStartUserTZ, Instant.now())
highlightDuration.text = highlightDurationForamtter.print(checkinDuration.toPeriod())

description.text = item.checkin.description
address.text = item.checkin.address
val startDate = checkInStartUserTZ.toLocalDate()
traceLocationCardHighlightView.setCaption(startDate.toString(DateTimeFormat.mediumDate()))

val autoCheckoutText = item.checkin.defaultCheckInLengthInMinutes?.let { checkoutLength ->
val checkoutAt = checkInStartUserTZ.plus(Duration.standardMinutes(checkoutLength.toLong()))
val checkoutIn = Duration(Instant.now(), checkoutAt).let {
val periodType = when {
it.isLongerThan(Duration.standardHours(1)) -> PeriodType.hours()
it.isLongerThan(Duration.standardDays(1)) -> PeriodType.days()
else -> PeriodType.minutes()
}
it.toPeriod(periodType)
}

context.getString(
R.string.trace_location_checkins_card_automatic_checkout_info,
checkInStartUserTZ.toLocalTime().toString("HH:mm"),
hourPeriodFormatter.print(checkoutIn)
)
}

checkoutInfo.text = autoCheckoutText ?: checkInStartUserTZ.toLocalTime().toString("HH:mm")

menuAction.setupMenu(R.menu.menu_trace_location_attendee_checkin_item) {
when (it.itemId) {
R.id.menu_remove_item -> item.onRemoveItem(item.checkin).let { true }
else -> false
}
}
}

data class Item(
val checkin: CheckIn,
val onCardClicked: (CheckIn) -> Unit,
val onRemoveItem: (CheckIn) -> Unit,
val onCheckout: (CheckIn) -> Unit,
) : CheckInsItem {
override val stableId: Long = checkin.id
}

companion object {
private val highlightDurationForamtter = PeriodFormatterBuilder().apply {
printZeroAlways()
minimumPrintedDigits(2)
appendHours()
appendSuffix(":")
appendMinutes()
}.toFormatter()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items

import android.view.Gravity
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import androidx.viewbinding.ViewBinding
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsAdapter

abstract class BaseCheckInVH<ItemT : CheckInsItem, BindingT : ViewBinding>(
parent: ViewGroup,
@LayoutRes layoutRes: Int
) : CheckInsAdapter.ItemVH<ItemT, BindingT>(
layoutRes = layoutRes,
parent = parent
) {

companion object {
fun View.setupMenu(@MenuRes menuRes: Int, onMenuAction: (MenuItem) -> Boolean) {
val menu = PopupMenu(context, this, Gravity.TOP or Gravity.END).apply {
inflate(menuRes)
setOnMenuItemClickListener { onMenuAction(it) }
}
setOnClickListener { menu.show() }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items

import de.rki.coronawarnapp.util.lists.HasStableId

interface CheckInsItem : HasStableId
Loading