From 071c6e43e5673fd79f7d7e00ea41600bfc55e344 Mon Sep 17 00:00:00 2001 From: Mohamed Metwalli <mohamed.metwalli@sap.com> Date: Thu, 17 Jun 2021 14:05:18 +0200 Subject: [PATCH] Camera permission card (EXPOSUREAPP-7879) (#3459) * Add camera permission card * Lint * Reorder code --- .../ui/overview/PersonOverviewAdapter.kt | 2 + .../ui/overview/PersonOverviewFragment.kt | 12 ++- .../overview/PersonOverviewFragmentEvents.kt | 1 + .../ui/overview/PersonOverviewViewModel.kt | 83 ++++++++++--------- .../ui/overview/items/CameraPermissionCard.kt | 32 +++++++ .../attendee/checkins/CheckInsFragment.kt | 21 +---- .../checkins/items/CameraPermissionVH.kt | 12 +-- .../util/ExternalActionHelper.kt | 22 +++++ ..._camera.xml => camera_permission_item.xml} | 0 9 files changed, 117 insertions(+), 68 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/items/CameraPermissionCard.kt rename Corona-Warn-App/src/main/res/layout/{trace_location_attendee_checkins_item_camera.xml => camera_permission_item.xml} (100%) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewAdapter.kt index 1f5a66255..aec69d34b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewAdapter.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.covidcertificate.person.ui.overview import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.viewbinding.ViewBinding +import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CameraPermissionCard import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CovidTestCertificatePendingCard import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CertificatesItem import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.PersonCertificateCard @@ -29,6 +30,7 @@ class PersonOverviewAdapter : CovidTestCertificatePendingCard(it) }, TypedVHCreatorMod({ data[it] is PersonCertificateCard.Item }) { PersonCertificateCard(it) }, + TypedVHCreatorMod({ data[it] is CameraPermissionCard.Item }) { CameraPermissionCard(it) }, ) ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragment.kt index e7a6f912a..106e4a95c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragment.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.covidcertificate.person.ui.overview import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.recyclerview.widget.DefaultItemAnimator @@ -10,9 +11,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import de.rki.coronawarnapp.R import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException +import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CameraPermissionCard import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CertificatesItem import de.rki.coronawarnapp.databinding.PersonOverviewFragmentBinding import de.rki.coronawarnapp.util.ExternalActionHelper.openUrl +import de.rki.coronawarnapp.util.ExternalActionHelper.openAppDetailsSettings import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator import de.rki.coronawarnapp.util.lists.diffutil.update @@ -38,7 +41,6 @@ class PersonOverviewFragment : Fragment(R.layout.person_overview_fragment), Auto } viewModel.personCertificates.observe(viewLifecycleOwner) { binding.bindViews(it) } viewModel.events.observe(viewLifecycleOwner) { onNavEvent(it) } - viewModel.markNewCertsAsSeen.observe(viewLifecycleOwner) { /** * This just needs to stay subscribed while the UI is open. @@ -84,6 +86,7 @@ class PersonOverviewFragment : Fragment(R.layout.person_overview_fragment), Auto "TODO \uD83D\uDEA7 Tomorrow maybe?!", Toast.LENGTH_LONG ).show() + OpenAppDeviceSettings -> openAppDetailsSettings() } } @@ -99,9 +102,10 @@ class PersonOverviewFragment : Fragment(R.layout.person_overview_fragment), Auto } } - private fun PersonOverviewFragmentBinding.bindViews(persons: List<CertificatesItem>) { - emptyLayout.isVisible = persons.isEmpty() - personOverviewAdapter.update(persons) + private fun PersonOverviewFragmentBinding.bindViews(items: List<CertificatesItem>) { + scanQrcodeFab.isGone = items.any { it is CameraPermissionCard.Item } + emptyLayout.isVisible = items.isEmpty() + personOverviewAdapter.update(items) } private fun PersonOverviewFragmentBinding.bindRecycler() = recyclerView.apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragmentEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragmentEvents.kt index 92702e837..6b8d4b704 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragmentEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewFragmentEvents.kt @@ -6,3 +6,4 @@ data class ShowRefreshErrorDialog(val error: Throwable) : PersonOverviewFragment data class ShowDeleteDialog(val certificateId: String) : PersonOverviewFragmentEvents() data class OpenPersonDetailsFragment(val personIdentifier: String) : PersonOverviewFragmentEvents() object ScanQrCode : PersonOverviewFragmentEvents() +object OpenAppDeviceSettings : PersonOverviewFragmentEvents() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewViewModel.kt index 320305628..b6d64f628 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/PersonOverviewViewModel.kt @@ -12,12 +12,14 @@ import de.rki.coronawarnapp.covidcertificate.person.core.PersonCertificates import de.rki.coronawarnapp.covidcertificate.person.core.PersonCertificatesProvider import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CertificatesItem import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CovidTestCertificatePendingCard +import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.CameraPermissionCard import de.rki.coronawarnapp.covidcertificate.person.ui.overview.items.PersonCertificateCard import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificate import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepository import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateIdentifier import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.permission.CameraPermissionProvider import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -37,15 +39,24 @@ class PersonOverviewViewModel @AssistedInject constructor( private val qrCodeGenerator: QrCodeGenerator, valueSetsRepository: ValueSetsRepository, @AppContext context: Context, + private val cameraPermissionProvider: CameraPermissionProvider, ) : CWAViewModel(dispatcherProvider) { + init { + valueSetsRepository.triggerUpdateValueSet(languageCode = context.getLocale()) + } + private val qrCodes = mutableMapOf<String, Bitmap?>() val events = SingleLiveEvent<PersonOverviewFragmentEvents>() val personCertificates: LiveData<List<CertificatesItem>> = combine( + cameraPermissionProvider.deniedPermanently, certificatesProvider.personCertificates, certificatesProvider.qrCodesFlow - ) { persons, qrCodesMap -> - mapPersons(persons, qrCodesMap) + ) { denied, persons, qrCodesMap -> + mutableListOf<CertificatesItem>().apply { + if (denied) add(CameraPermissionCard.Item { events.postValue(OpenAppDeviceSettings) }) + addPersonItems(persons, qrCodesMap) + } }.asLiveData(dispatcherProvider.Default) val markNewCertsAsSeen = testCertificateRepository.certificates @@ -60,15 +71,21 @@ class PersonOverviewViewModel @AssistedInject constructor( .catch { Timber.w("Failed to mark certificates as seen.") } .asLiveData2() - init { - valueSetsRepository.triggerUpdateValueSet(languageCode = context.getLocale()) + fun deleteTestCertificate(identifier: TestCertificateIdentifier) = launch { + testCertificateRepository.deleteCertificate(identifier) } - private fun mapPersons(persons: Set<PersonCertificates>, qrCodesMap: Map<String, Bitmap?>): List<CertificatesItem> = - mutableListOf<CertificatesItem>().apply { - addPendingCards(persons) - addCertificateCards(persons, qrCodesMap) - } + fun onScanQrCode() = events.postValue(ScanQrCode) + + fun checkCameraSettings() = cameraPermissionProvider.checkSettings() + + private fun MutableList<CertificatesItem>.addPersonItems( + persons: Set<PersonCertificates>, + qrCodesMap: Map<String, Bitmap?> + ) { + addPendingCards(persons) + addCertificateCards(persons, qrCodesMap) + } private fun MutableList<CertificatesItem>.addCertificateCards( persons: Set<PersonCertificates>, @@ -81,22 +98,14 @@ class PersonOverviewViewModel @AssistedInject constructor( PersonCertificateCard.Item( certificate = certificate, qrcodeBitmap = qrCodes[certificate.qrCode], - color = PersonOverviewItemColor.colorFor(index), - onClickAction = { - events.postValue( - OpenPersonDetailsFragment(person.personIdentifier.codeSHA256) - ) - } - ) + color = PersonOverviewItemColor.colorFor(index) + ) { + events.postValue(OpenPersonDetailsFragment(person.personIdentifier.codeSHA256)) + } ) } } - private fun PersonCertificates.hasPendingTestCertificate(): Boolean { - val certificate = highestPriorityCertificate - return certificate is TestCertificate && certificate.isCertificateRetrievalPending - } - private fun MutableList<CertificatesItem>.addPendingCards(persons: Set<PersonCertificates>) { persons.forEach { val certificate = it.highestPriorityCertificate @@ -112,17 +121,21 @@ class PersonOverviewViewModel @AssistedInject constructor( } } + private fun PersonCertificates.hasPendingTestCertificate(): Boolean { + val certificate = highestPriorityCertificate + return certificate is TestCertificate && certificate.isCertificateRetrievalPending + } + private val PersonCertificatesProvider.qrCodesFlow - get() = personCertificates - .transform { persons -> - emit(emptyMap()) // Initial state - persons.filterNotPending() - .forEach { - val qrCode = it.highestPriorityCertificate.qrCode - qrCodes[qrCode] = generateQrCode(qrCode) - emit(qrCodes) - } - } + get() = personCertificates.transform { persons -> + emit(qrCodes) // Initial state + persons.filterNotPending() + .forEach { + val qrCode = it.highestPriorityCertificate.qrCode + qrCodes[qrCode] = generateQrCode(qrCode) + emit(qrCodes) + } + } private fun Set<PersonCertificates>.filterNotPending() = this .filter { !it.hasPendingTestCertificate() } @@ -141,14 +154,6 @@ class PersonOverviewViewModel @AssistedInject constructor( error?.let { events.postValue(ShowRefreshErrorDialog(error)) } } - fun deleteTestCertificate(identifier: TestCertificateIdentifier) = launch { - testCertificateRepository.deleteCertificate(identifier) - } - - fun onScanQrCode() { - events.postValue(ScanQrCode) - } - @AssistedFactory interface Factory : SimpleCWAViewModelFactory<PersonOverviewViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/items/CameraPermissionCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/items/CameraPermissionCard.kt new file mode 100644 index 000000000..38ec4477f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/ui/overview/items/CameraPermissionCard.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.covidcertificate.person.ui.overview.items + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.covidcertificate.person.ui.overview.PersonOverviewAdapter +import de.rki.coronawarnapp.databinding.CameraPermissionItemBinding +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class CameraPermissionCard(parent: ViewGroup) : + PersonOverviewAdapter.PersonOverviewItemVH<CameraPermissionCard.Item, CameraPermissionItemBinding>( + layoutRes = R.layout.camera_permission_item, + parent = parent + ) { + + override val viewBinding: Lazy<CameraPermissionItemBinding> = lazy { CameraPermissionItemBinding.bind(itemView) } + + override val onBindData: CameraPermissionItemBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + openSettings.setOnClickListener { curItem.onOpenSettings() } + itemView.setOnClickListener { curItem.onOpenSettings() } + } + + data class Item( + val onOpenSettings: () -> Unit + ) : CertificatesItem, HasPayloadDiffer { + override val stableId: Long = Item::class.simpleName.hashCode().toLong() + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt index 73735fe03..ff61d7114 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt @@ -1,10 +1,7 @@ package de.rki.coronawarnapp.ui.presencetracing.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.widget.Toolbar @@ -18,13 +15,13 @@ import androidx.recyclerview.widget.DefaultItemAnimator import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.Hold import com.google.android.material.transition.MaterialSharedAxis -import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsFragmentBinding import de.rki.coronawarnapp.presencetracing.checkins.CheckIn import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.items.CheckInsItem import de.rki.coronawarnapp.ui.presencetracing.attendee.edit.EditCheckInFragmentArgs +import de.rki.coronawarnapp.util.ExternalActionHelper.openAppDetailsSettings import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.list.setupSwipe import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator @@ -121,7 +118,7 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag setupAxisTransition() doNavigate(CheckInsFragmentDirections.actionCheckInsFragmentToCheckInOnboardingFragment(false)) } - is CheckInEvent.OpenDeviceSettings -> openDeviceSettings() + is CheckInEvent.OpenDeviceSettings -> openAppDetailsSettings() is CheckInEvent.InvalidQrCode -> showInvalidQrCodeInformation(event.errorText) } } @@ -146,20 +143,6 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } } - 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 { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/CameraPermissionVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/CameraPermissionVH.kt index c0dd87bca..ccbabaa49 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/CameraPermissionVH.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/CameraPermissionVH.kt @@ -2,19 +2,19 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.items import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsItemCameraBinding +import de.rki.coronawarnapp.databinding.CameraPermissionItemBinding class CameraPermissionVH(parent: ViewGroup) : - BaseCheckInVH<CameraPermissionVH.Item, TraceLocationAttendeeCheckinsItemCameraBinding>( - layoutRes = R.layout.trace_location_attendee_checkins_item_camera, + BaseCheckInVH<CameraPermissionVH.Item, CameraPermissionItemBinding>( + layoutRes = R.layout.camera_permission_item, parent = parent ) { - override val viewBinding: Lazy<TraceLocationAttendeeCheckinsItemCameraBinding> = lazy { - TraceLocationAttendeeCheckinsItemCameraBinding.bind(itemView) + override val viewBinding: Lazy<CameraPermissionItemBinding> = lazy { + CameraPermissionItemBinding.bind(itemView) } - override val onBindData: TraceLocationAttendeeCheckinsItemCameraBinding.( + override val onBindData: CameraPermissionItemBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, _ -> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ExternalActionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ExternalActionHelper.kt index acc91f7c4..8760d3522 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ExternalActionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ExternalActionHelper.kt @@ -1,14 +1,19 @@ package de.rki.coronawarnapp.util +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings +import android.widget.Toast import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.BuildConfig +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExternalActionException import de.rki.coronawarnapp.exception.reporting.report +import timber.log.Timber /** * Helper object for external actions @@ -122,4 +127,21 @@ object ExternalActionHelper { ) } } + + /** + * Open App's device details settings such as permissions + */ + fun Fragment.openAppDetailsSettings() { + 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() + } + } } diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_item_camera.xml b/Corona-Warn-App/src/main/res/layout/camera_permission_item.xml similarity index 100% rename from Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_item_camera.xml rename to Corona-Warn-App/src/main/res/layout/camera_permission_item.xml -- GitLab