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

Camera permission card (EXPOSUREAPP-5786) #2713

Merged
merged 70 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
e1a9b4d
Implement swipe to delete
mtwalli Mar 24, 2021
468a9a4
Swipe to delete in CheckIns screen
mtwalli Mar 24, 2021
fb5fcba
Reduce parameters and consider non swiping items
mtwalli Mar 24, 2021
330e651
lint
mtwalli Mar 24, 2021
305f0c1
lint
mtwalli Mar 24, 2021
62ed7ba
Update docs
mtwalli Mar 24, 2021
3bb5865
Add TODO
mtwalli Mar 24, 2021
c3822b1
Update ic_delete.xml
mtwalli Mar 24, 2021
815d6f5
Create camera card
mtwalli Mar 24, 2021
b5a8204
Merge branch 'release/2.0.x' into feature/5768-swipe-to-delete
harambasicluka Mar 24, 2021
8735dd3
Add card strings
mtwalli Mar 24, 2021
7b23d61
use strings in layout
mtwalli Mar 24, 2021
63ce859
Merge branch 'release/2.0.x' into feature/5768-swipe-to-delete
harambasicluka Mar 24, 2021
fa430b5
Merge branch 'release/2.0.x' into feature/5768-swipe-to-delete
mtwalli Mar 24, 2021
1d950ec
Rounded background
mtwalli Mar 24, 2021
624f151
Increase icon size proportionally
mtwalli Mar 24, 2021
d0f6a22
Implemente Swipe contract
mtwalli Mar 24, 2021
6ae9fc7
Handle dismiss for swipe
mtwalli Mar 24, 2021
556c162
Merge branch 'release/2.0.x' into feature/5768-swipe-to-delete
mtwalli Mar 24, 2021
a7742ee
Animate FAB onScroll
mtwalli Mar 24, 2021
5f65739
Merge branch 'feature/5768-swipe-to-delete' of https://github.com/cor…
mtwalli Mar 24, 2021
6292e53
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 24, 2021
6ec2715
Merge branch 'feature/5768-swipe-to-delete' into feature/5786-camera-…
mtwalli Mar 24, 2021
45a3809
Create flow of permission status
mtwalli Mar 24, 2021
f1cc50d
Avoid drawing out of screen when view is completely swiped
mtwalli Mar 25, 2021
0395b47
klint
mtwalli Mar 25, 2021
424810f
Merge branch 'feature/5768-swipe-to-delete' into feature/5786-camera-…
mtwalli Mar 25, 2021
11db63a
Pass flag from fragment
mtwalli Mar 25, 2021
53efe97
Rearrange code
mtwalli Mar 25, 2021
ee95f9b
Rename color
mtwalli Mar 25, 2021
6c71efd
Fix qr-code icon
mtwalli Mar 25, 2021
3660924
Merge branch 'release/2.0.x' into feature/5768-swipe-to-delete
mtwalli Mar 25, 2021
3c47c75
Nav bar icon
mtwalli Mar 25, 2021
4a0ce1a
Notify item when dialog is canceled
mtwalli Mar 25, 2021
a8d34d8
Merge branch 'feature/5768-swipe-to-delete' into feature/5786-camera-…
mtwalli Mar 25, 2021
9e164c1
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 26, 2021
01947d9
Reduce lines
mtwalli Mar 26, 2021
7ec047d
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 26, 2021
b261178
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 29, 2021
efb7fc5
Small refactoring
mtwalli Mar 29, 2021
f052ea0
Rely on settings
mtwalli Mar 29, 2021
15107a1
Unit tests
mtwalli Mar 29, 2021
cad7485
Fix text
mtwalli Mar 29, 2021
6560340
lint
mtwalli Mar 29, 2021
9860495
clean
mtwalli Mar 29, 2021
387d0a4
Renaming
mtwalli Mar 29, 2021
233808f
Smaller functions
mtwalli Mar 29, 2021
184b161
Add test
mtwalli Mar 29, 2021
2734cc4
Shrink lines
mtwalli Mar 29, 2021
fab7ded
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 29, 2021
ad69a17
revert strings changes
mtwalli Mar 29, 2021
e3da3ad
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 30, 2021
affd519
Rename to CameraSettings
mtwalli Mar 30, 2021
08f2bfa
renaming
mtwalli Mar 30, 2021
9116e3c
Handle exception
mtwalli Mar 30, 2021
491db4e
Reset flag if rationale can be shown again
mtwalli Mar 30, 2021
e4cc9a3
create Movable interface
mtwalli Mar 30, 2021
1cb5c00
lint
mtwalli Mar 30, 2021
ae22343
extra line
mtwalli Mar 30, 2021
a0bc3b4
Tests
mtwalli Mar 30, 2021
7f94ddd
More tests
mtwalli Mar 30, 2021
da17987
More tests
mtwalli Mar 30, 2021
43d4cbc
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 30, 2021
0387bca
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
harambasicluka Mar 30, 2021
895ea64
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 31, 2021
4d1581e
Check camera settings onResume
mtwalli Mar 31, 2021
ebe43da
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
harambasicluka Mar 31, 2021
f4a72bf
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 31, 2021
557fa9b
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 31, 2021
e4f2a4e
Merge branch 'release/2.0.x' into feature/5786-camera-permission-card
mtwalli Mar 31, 2021
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
Expand Up @@ -16,4 +16,6 @@ sealed class CheckInEvent {
data class ConfirmSwipeItem(val checkIn: CheckIn, val position: Int) : CheckInEvent()

object ShowInformation : CheckInEvent()

object OpenDeviceSettings : CheckInEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.CameraPermissionVH
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
Expand All @@ -27,6 +28,7 @@ class CheckInsAdapter :
DataBinderMod<CheckInsItem, ItemVH<CheckInsItem, ViewBinding>>(data),
TypedVHCreatorMod({ data[it] is ActiveCheckInVH.Item }) { ActiveCheckInVH(it) },
TypedVHCreatorMod({ data[it] is PastCheckInVH.Item }) { PastCheckInVH(it) },
TypedVHCreatorMod({ data[it] is CameraPermissionVH.Item }) { CameraPermissionVH(it) },
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins

import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
Expand All @@ -15,9 +18,12 @@ 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.BuildConfig
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsFragmentBinding
import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem
import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.list.isSwipeable
Expand All @@ -31,6 +37,7 @@ import de.rki.coronawarnapp.util.ui.observe2
import de.rki.coronawarnapp.util.ui.viewBindingLazy
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
import timber.log.Timber
import javax.inject.Inject

class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_fragment), AutoInject {
Expand All @@ -42,7 +49,10 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
factoryProducer = { viewModelFactory },
constructorCall = { factory, savedState ->
factory as CheckInsViewModel.Factory
factory.create(savedState = savedState, deepLink = navArgs.uri)
factory.create(
mtwalli marked this conversation as resolved.
Show resolved Hide resolved
savedState = savedState,
deepLink = navArgs.uri
)
}
)
private val binding: TraceLocationAttendeeCheckinsFragmentBinding by viewBindingLazy()
Expand All @@ -57,33 +67,79 @@ 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()
onScroll { extend ->
if (extend) binding.scanCheckinQrcodeFab.extend() else binding.scanCheckinQrcodeFab.shrink()
}
bindRecycler()
bindFAB()

onSwipeItem(
context = requireContext(),
excludedPositions = listOf() // TODO exclude items from swiping such as Camera permission item
) { position, direction ->
val checkInsItem = checkInsAdapter.data[position]
if (checkInsItem.isSwipeable()) {
checkInsItem.onSwipe(position, direction)
}
}
viewModel.checkins.observe2(this) { items ->
updateViews(items)
}

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

viewModel.errorEvent.observe2(this) {
val errorForHumans = it.tryHumanReadableError(requireContext())
Toast.makeText(requireContext(), errorForHumans.description, Toast.LENGTH_LONG).show()
}
}

private fun onNavigationEvent(event: CheckInEvent?) {
when (event) {
is CheckInEvent.ConfirmCheckIn -> doNavigate(
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(
verifiedTraceLocation = event.verifiedTraceLocation,
editCheckInId = 0,
)
)

is CheckInEvent.ConfirmSwipeItem -> showRemovalConfirmation(event.checkIn, event.position)

is CheckInEvent.ConfirmRemoveItem -> showRemovalConfirmation(event.checkIn, null)

is CheckInEvent.ConfirmRemoveAll -> showRemovalConfirmation(null, null)

is CheckInEvent.EditCheckIn -> doNavigate(
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(
verifiedTraceLocation = null,
editCheckInId = event.checkInId,
)
)

is CheckInEvent.ShowInformation -> Toast.makeText(
requireContext(),
"TODO ¯\\_(ツ)_/¯",
Toast.LENGTH_SHORT
).show()

is CheckInEvent.OpenDeviceSettings -> openDeviceSettings()
}
}

private fun updateViews(items: List<CheckInsItem>) {
checkInsAdapter.update(items)
binding.apply {
scanCheckinQrcodeFab.isGone = items.any { it is CameraPermissionVH.Item }
emptyListInfoContainer.isGone = items.isNotEmpty()
checkInsList.isGone = items.isEmpty()
}
}

private fun openDeviceSettings() {
try {
startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
)
)
} catch (e: ActivityNotFoundException) {
Timber.e(e, "Could not open device settings")
Toast.makeText(requireContext(), R.string.errors_generic_headline, Toast.LENGTH_LONG).show()
}
}

private fun bindFAB() {
binding.scanCheckinQrcodeFab.apply {
setOnClickListener {
findNavController().navigate(
Expand All @@ -106,44 +162,28 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
}
}
}
}

viewModel.events.observe2(this) {
when (it) {
is CheckInEvent.ConfirmCheckIn -> {
doNavigate(
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(
verifiedTraceLocation = it.verifiedTraceLocation,
editCheckInId = 0,
)
)
}
private fun bindRecycler() {
binding.checkInsList.apply {
adapter = checkInsAdapter
addItemDecoration(TopBottomPaddingDecorator(topPadding = R.dimen.spacing_tiny))
itemAnimator = DefaultItemAnimator()

is CheckInEvent.ConfirmSwipeItem -> {
showRemovalConfirmation(it.checkIn, it.position)
}
is CheckInEvent.ConfirmRemoveItem -> {
showRemovalConfirmation(it.checkIn, null)
}
is CheckInEvent.ConfirmRemoveAll -> {
showRemovalConfirmation(null, null)
}
is CheckInEvent.EditCheckIn -> {
doNavigate(
CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragment(
verifiedTraceLocation = null,
editCheckInId = it.checkInId,
)
)
}
is CheckInEvent.ShowInformation -> {
Toast.makeText(requireContext(), "TODO ¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show()
with(binding.scanCheckinQrcodeFab) {
onScroll { extend ->
if (extend) extend() else shrink()
}
}
}

viewModel.errorEvent.observe2(this) {
val errorForHumans = it.tryHumanReadableError(requireContext())
Toast.makeText(requireContext(), errorForHumans.description, Toast.LENGTH_LONG).show()
onSwipeItem(
context = requireContext(),
) { position, direction ->
val checkInsItem = checkInsAdapter.data[position]
if (checkInsItem.isSwipeable()) {
checkInsItem.onSwipe(position, direction)
}
}
}
}

Expand All @@ -158,19 +198,11 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
viewModel.onRemoveCheckInConfirmed(checkIn)
}
setNegativeButton(R.string.generic_action_abort) { _, _ ->
position?.let {
checkInsAdapter.notifyItemChanged(
position
)
}
position?.let { checkInsAdapter.notifyItemChanged(position) }
}

setOnCancelListener {
position?.let {
checkInsAdapter.notifyItemChanged(
position
)
}
position?.let { checkInsAdapter.notifyItemChanged(position) }
}
}.show()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationQRCod
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.CameraPermissionVH
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.PastCheckInVH
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.permission.CameraPermissionProvider
import de.rki.coronawarnapp.util.coroutine.AppScope
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.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import timber.log.Timber

class CheckInsViewModel @AssistedInject constructor(
Expand All @@ -34,50 +36,12 @@ class CheckInsViewModel @AssistedInject constructor(
private val qrCodeUriParser: QRCodeUriParser,
private val checkInsRepository: CheckInRepository,
private val checkOutHandler: CheckOutHandler,
cameraPermissionProvider: CameraPermissionProvider
) : CWAViewModel(dispatcherProvider) {

val events = SingleLiveEvent<CheckInEvent>()
val errorEvent = SingleLiveEvent<Throwable>()

val checkins: LiveData<List<CheckInsItem>> = checkInsRepository.allCheckIns
.map { checkins ->
checkins.sortedWith(compareBy<CheckIn> { it.completed }.thenByDescending { it.checkInEnd })
}
.map { checkins ->
checkins.map { checkin ->
when {
!checkin.completed -> ActiveCheckInVH.Item(
checkin = checkin,
onCardClicked = { events.postValue(CheckInEvent.EditCheckIn(it.id)) },
onRemoveItem = { events.postValue(CheckInEvent.ConfirmRemoveItem(it)) },
onCheckout = { doCheckOutNow(it) },
onSwipeItem = { checkIn, position ->
events.postValue(
CheckInEvent.ConfirmSwipeItem(
checkIn,
position
)
)
}
)
else -> PastCheckInVH.Item(
checkin = checkin,
onCardClicked = { events.postValue(CheckInEvent.EditCheckIn(it.id)) },
onRemoveItem = { events.postValue(CheckInEvent.ConfirmRemoveItem(it)) },
onSwipeItem = { checkIn, position ->
events.postValue(
CheckInEvent.ConfirmSwipeItem(
checkIn,
position
)
)
}
)
}
}
}
.asLiveData(context = dispatcherProvider.Default)

init {
deepLink?.let {
if (deepLink != savedState.get(SKEY_LAST_DEEPLINK)) {
Expand All @@ -90,15 +54,19 @@ class CheckInsViewModel @AssistedInject constructor(
savedState.set(SKEY_LAST_DEEPLINK, deepLink)
}

private fun doCheckOutNow(checkIn: CheckIn) = launch(scope = appScope) {
Timber.d("doCheckOutNow(checkIn=%s)", checkIn)
try {
checkOutHandler.checkOut(checkIn.id)
} catch (e: Exception) {
Timber.e(e, "Checkout failed for %s", checkIn)
errorEvent.postValue(e)
val checkins: LiveData<List<CheckInsItem>> = combine(
checkInsRepository.allCheckIns,
cameraPermissionProvider.deniedPermanently
) { checkIns, denied ->
mutableListOf<CheckInsItem>().apply {
// Camera permission item
if (denied) {
add(cameraPermissionItem())
}
// CheckIns items
addAll(mapCheckIns(checkIns))
}
}
}.asLiveData(context = dispatcherProvider.Default)

fun onRemoveCheckInConfirmed(checkIn: CheckIn?) {
Timber.d("removeCheckin(checkIn=%s)", checkIn)
Expand All @@ -116,6 +84,46 @@ class CheckInsViewModel @AssistedInject constructor(
events.postValue(CheckInEvent.ConfirmRemoveAll)
}

private fun cameraPermissionItem() = CameraPermissionVH.Item(
onOpenSettings = {
events.postValue(CheckInEvent.OpenDeviceSettings)
}
)

private fun mapCheckIns(checkIns: List<CheckIn>): List<CheckInsItem> = checkIns
.sortedWith(compareBy<CheckIn> { it.completed }.thenByDescending { it.checkInEnd })
.map { checkin ->
when {
!checkin.completed -> ActiveCheckInVH.Item(
checkin = checkin,
onCardClicked = { events.postValue(CheckInEvent.EditCheckIn(it.id)) },
onRemoveItem = { events.postValue(CheckInEvent.ConfirmRemoveItem(it)) },
onCheckout = { doCheckOutNow(it) },
onSwipeItem = { checkIn, position ->
events.postValue(CheckInEvent.ConfirmSwipeItem(checkIn, position))
}
)
else -> PastCheckInVH.Item(
checkin = checkin,
onCardClicked = { events.postValue(CheckInEvent.EditCheckIn(it.id)) },
onRemoveItem = { events.postValue(CheckInEvent.ConfirmRemoveItem(it)) },
onSwipeItem = { checkIn, position ->
events.postValue(CheckInEvent.ConfirmSwipeItem(checkIn, position))
}
)
}
}

private fun doCheckOutNow(checkIn: CheckIn) = launch(scope = appScope) {
Timber.d("doCheckOutNow(checkIn=%s)", checkIn)
try {
checkOutHandler.checkOut(checkIn.id)
} catch (e: Exception) {
Timber.e(e, "Checkout failed for %s", checkIn)
errorEvent.postValue(e)
}
}

private fun verifyUri(uri: String) = launch {
try {
Timber.i("uri: $uri")
Expand Down
Loading