diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt new file mode 100644 index 00000000000..039b5d6bd77 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.ui.color + +import android.graphics.Color +import io.kotest.matchers.shouldBe +import org.junit.Test + +import testhelpers.BaseTestInstrumentation + +class ColorTest : BaseTestInstrumentation() { + + @Test + fun parseValidColor() { + "#FFFFFF".parseColor() shouldBe Color.WHITE + } + + @Test + fun parseInvalidColor() { + "000000".parseColor() shouldBe Color.BLACK + } + + @Test + fun defaultColor() { + "00".parseColor(Color.GRAY) shouldBe Color.GRAY + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt index dc916befbe2..70f15828db4 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt @@ -11,7 +11,6 @@ import androidx.core.text.color import androidx.core.text.scale import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation @@ -36,29 +35,6 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - with(binding) { - scanCheckInQrCode.setOnClickListener { - doNavigate( - EventRegistrationTestFragmentDirections - .actionEventRegistrationTestFragmentToScanCheckInQrCodeFragment() - ) - } - - testQrCodeCreation.setOnClickListener { - doNavigate( - EventRegistrationTestFragmentDirections - .actionEventRegistrationTestFragmentToTestQrCodeCreationFragment() - ) - } - - createEventButton.setOnClickListener { - findNavController().navigate(R.id.createEventTestFragment) - } - - showEventsButton.setOnClickListener { - findNavController().navigate(R.id.showStoredEventsTestFragment) - } - } binding.runMatcher.setOnClickListener { viewModel.runMatcher() } @@ -94,6 +70,12 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis lastOrganiserLocation.text = traceLocationText(traceLocation) lastOrganiserLocationId.text = styleText("ID", traceLocation.locationId.base64()) lastOrganiserLocationUrl.text = styleText("URL", traceLocation.locationUrl) + qrcodeButton.setOnClickListener { + doNavigate( + EventRegistrationTestFragmentDirections + .actionEventRegistrationTestFragmentToQrCodePosterFragmentTest(traceLocation.id) + ) + } } } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt deleted file mode 100644 index 74d32337209..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.createevent - -import android.os.Bundle -import android.view.View -import android.widget.ArrayAdapter -import android.widget.AutoCompleteTextView -import androidx.core.widget.doAfterTextChanged -import androidx.core.widget.doOnTextChanged -import androidx.fragment.app.Fragment -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.util.hideKeyboard -import de.rki.coronawarnapp.databinding.FragmentTestCreateeventBinding -import de.rki.coronawarnapp.util.di.AutoInject -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.cwaViewModels -import timber.log.Timber -import javax.inject.Inject - -class CreateEventTestFragment : Fragment(R.layout.fragment_test_createevent), AutoInject { - - @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val vm: CreateEventTestViewModel by cwaViewModels { viewModelFactory } - - private val binding: FragmentTestCreateeventBinding by viewBindingLazy() - - private val eventString = "Event" - private val locationString = "Location" - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - initSpinner() - initOnCreateEventClicked() - observeViewModelResult() - } - - private fun observeViewModelResult() { - vm.result.observe2(this) { - when (it) { - is CreateEventTestViewModel.Result.Success -> - binding.resultText.text = "Successfully stored: ${it.eventEntity}" - is CreateEventTestViewModel.Result.Error -> - binding.resultText.text = "There is something wrong with your input values, please check again." - } - } - } - - private fun initOnCreateEventClicked() = with(binding) { - createEventButton.setOnClickListener { - createEvent() - it.hideKeyboard() - } - } - - private fun FragmentTestCreateeventBinding.createEvent() { - vm.createEvent( - eventOrLocationSpinner.editText!!.text.toString(), - eventDescription.text.toString(), - eventAddress.text.toString(), - eventStartEditText.text.toString(), - eventEndEditText.text.toString(), - eventDefaultCheckinLengthInMinutes.text.toString() - ) - } - - private fun initSpinner() { - val items = listOf(eventString, locationString) - with(binding.eventOrLocationSpinner.editText as AutoCompleteTextView) { - setText(items.first(), false) - setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, items)) - doAfterTextChanged { } - doOnTextChanged { text, start, before, count -> - Timber.d("text: $text, start: $start, before: $before, count: $count") - - when (text.toString()) { - eventString -> { - binding.eventStart.visibility = View.VISIBLE - binding.eventEnd.visibility = View.VISIBLE - } - locationString -> { - binding.eventStart.visibility = View.GONE - binding.eventEnd.visibility = View.GONE - binding.eventStartEditText.text = null - binding.eventEndEditText.text = null - } - } - } - } - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt deleted file mode 100644 index 258a96c7ad4..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.createevent - -import dagger.Binds -import dagger.Module -import dagger.multibindings.IntoMap -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory -import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey - -@Module -abstract class CreateEventTestFragmentModule { - - @Binds - @IntoMap - @CWAViewModelKey(CreateEventTestViewModel::class) - abstract fun testCreateEventFragment( - factory: CreateEventTestViewModel.Factory - ): CWAViewModelFactory -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt deleted file mode 100644 index 66a5cabd01d..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt +++ /dev/null @@ -1,73 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.createevent - -import androidx.lifecycle.MutableLiveData -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation -import de.rki.coronawarnapp.eventregistration.events.TraceLocationCreator -import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER -import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormat -import timber.log.Timber - -class CreateEventTestViewModel @AssistedInject constructor( - dispatcherProvider: DispatcherProvider, - private val traceLocationCreator: TraceLocationCreator -) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - - @AssistedFactory - interface Factory : SimpleCWAViewModelFactory - - val result = MutableLiveData() - - fun createEvent( - type: String, - description: String, - address: String, - start: String, - end: String, - defaultCheckInLengthInMinutes: String - ) { - try { - val startDate = - if (start.isBlank()) null else DateTime.parse(start, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm")) - val endDate = - if (end.isBlank()) null else DateTime.parse(end, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm")) - - val traceLocationType = - if (type == "Event") LOCATION_TYPE_TEMPORARY_OTHER else LOCATION_TYPE_PERMANENT_OTHER - - val userInput = TraceLocationUserInput( - traceLocationType, - description, - address, - startDate?.toInstant(), - endDate?.toInstant(), - defaultCheckInLengthInMinutes.toInt() - ) - - launch { - try { - val traceLocation = traceLocationCreator.createTraceLocation(userInput) - result.postValue(Result.Success(traceLocation)) - } catch (exception: Exception) { - Timber.d("Something went wrong when creating the trace location $exception") - result.postValue(Result.Error) - } - } - } catch (exception: Exception) { - Timber.d("Something went wrong when creating the trace location $exception") - result.postValue(Result.Error) - } - } - - sealed class Result { - object Error : Result() - data class Success(val eventEntity: TraceLocation) : Result() - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt deleted file mode 100644 index 0124e0857c0..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.qrcode - -import android.annotation.SuppressLint -import android.os.Bundle -import android.print.PrintAttributes -import android.print.PrintManager -import android.view.View -import android.widget.Toast -import androidx.core.content.getSystemService -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.FragmentTestQrcodeCreationBinding -import de.rki.coronawarnapp.test.eventregistration.ui.PrintingAdapter -import de.rki.coronawarnapp.util.di.AutoInject -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.cwaViewModels -import timber.log.Timber -import javax.inject.Inject - -class QrCodeCreationTestFragment : Fragment(R.layout.fragment_test_qrcode_creation), AutoInject { - - @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - - private val viewModel: QrCodeCreationTestViewModel by cwaViewModels { viewModelFactory } - private val binding: FragmentTestQrcodeCreationBinding by viewBindingLazy() - - @SuppressLint("SetTextI18n") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - - viewModel.sharingIntent.observe2(this) { fileIntent -> - - binding.printPDF.isVisible = true - binding.printPDF.setOnClickListener { - // Context must be an Activity context - val printingManger = context?.getSystemService() - Timber.i("PrintingManager: $printingManger") - printingManger?.apply { - val printingJob = print( - "CoronaWarnApp", - PrintingAdapter(fileIntent.file), - PrintAttributes - .Builder() - .setMediaSize(PrintAttributes.MediaSize.ISO_A3) - .build() - ) - - Timber.i("PrintingJob:$printingJob") - Timber.i("PrintingJob isBlocked:${printingJob.isBlocked}") - Timber.i("PrintingJob isCancelled:${printingJob.isCancelled}") - Timber.i("PrintingJob isCompleted:${printingJob.isCompleted}") - Timber.i("PrintingJob isFailed:${printingJob.isFailed}") - Timber.i("PrintingJob info:${printingJob.info}") - } - } - binding.sharePDF.isVisible = true - binding.sharePDF.setOnClickListener { - startActivity(fileIntent.intent(requireActivity())) - } - } - - viewModel.qrCodeBitmap.observe2(this) { - binding.qrCodeImage.setImageBitmap(it) - if (it != null) { - viewModel.createPDF(binding.pdfPage) - } - } - - viewModel.errorMessage.observe2(this) { - Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() - } - - binding.qrCodeText.setText( - "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUD" + - "BOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFF" + - "BU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" - ) - binding.generateQrCode.setOnClickListener { - viewModel.createQrCode(binding.qrCodeText.text.toString()) - } - - binding.downloadQrCodePosterTemplate.setOnClickListener { - viewModel.downloadQrCodePosterTemplate() - } - - viewModel.qrCodePosterTemplate.observe2(this) { vectorDrawableBytes -> - binding.downloadedQrCodePoster.text = vectorDrawableBytes.utf8() - } - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt deleted file mode 100644 index 336f7fc486d..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt +++ /dev/null @@ -1,104 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.qrcode - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.pdf.PdfDocument -import android.view.View -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer -import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeGenerator -import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.files.FileSharing -import de.rki.coronawarnapp.util.ui.SingleLiveEvent -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import okio.ByteString -import okio.ByteString.Companion.toByteString -import timber.log.Timber -import java.io.File -import java.io.FileOutputStream - -class QrCodeCreationTestViewModel @AssistedInject constructor( - private val dispatcher: DispatcherProvider, - private val fileSharing: FileSharing, - private val qrCodeGenerator: QrCodeGenerator, - @AppContext private val context: Context, - private val posterTemplateServer: QrCodePosterTemplateServer -) : CWAViewModel(dispatcher) { - - val qrCodeBitmap = SingleLiveEvent() - val errorMessage = SingleLiveEvent() - val sharingIntent = SingleLiveEvent() - val qrCodePosterTemplate = SingleLiveEvent() - - /** - * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap] - */ - fun createQrCode(input: String) = launch(context = dispatcher.IO) { - - try { - qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input)) - } catch (e: Exception) { - Timber.d(e, "Qr code creation failed") - errorMessage.postValue(e.localizedMessage ?: "QR code creation failed") - } - } - - /** - * Create a new PDF file and result is delivered by [sharingIntent] - * as a sharing [FileSharing.ShareIntentProvider] - */ - fun createPDF( - view: View - ) = launch(context = dispatcher.IO) { - try { - val file = pdfFile() - val pageInfo = PdfDocument.PageInfo.Builder( - view.width, - view.height, - 1 - ).create() - - PdfDocument().apply { - startPage(pageInfo).apply { - view.draw(canvas) - finishPage(this) - } - - FileOutputStream(file).use { - writeTo(it) - close() - } - } - - sharingIntent.postValue( - fileSharing.getFileIntentProvider(file, "Scan and Help") - ) - } catch (e: Exception) { - errorMessage.postValue(e.localizedMessage ?: "Creating pdf failed") - Timber.d(e, "Creating pdf failed") - } - } - - private fun pdfFile(): File { - val dir = File(context.filesDir, "events") - if (!dir.exists()) dir.mkdirs() - return File(dir, "CoronaWarnApp-Event.pdf") - } - - fun downloadQrCodePosterTemplate() { - launch { - try { - val posterTemplate = posterTemplateServer.downloadQrCodePosterTemplate() - qrCodePosterTemplate.postValue(posterTemplate.template.toByteArray().toByteString()) - } catch (exception: Exception) { - errorMessage.postValue("Downloading Poster Template failed: ${exception.message}") - } - } - } - - @AssistedFactory - interface Factory : SimpleCWAViewModelFactory -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt deleted file mode 100644 index 77b7135aeef..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.showevents - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.FragmentTestShowstoredeventsBinding -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation -import de.rki.coronawarnapp.util.di.AutoInject -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.cwaViewModels -import javax.inject.Inject - -class ShowStoredEventsTestFragment : Fragment(R.layout.fragment_test_showstoredevents), AutoInject { - - @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val vm: ShowStoredEventsTestViewModel by cwaViewModels { viewModelFactory } - - private val binding: FragmentTestShowstoredeventsBinding by viewBindingLazy() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - vm.storedEvents.observe2(this) { events -> - binding.storedEvents.text = events.joinToString(separator = "\n\n") { it.getSimpleUIString() } - } - - binding.deleteAllEvents.setOnClickListener { - vm.deleteAllEvents() - } - } - - private fun TraceLocation.getSimpleUIString(): String { - return listOf( - "id = $id", - "type = $type", - "description = $description", - "location = $address", - "startTime = $startDate", - "endTime = $endDate", - "defaultCheckInLengthInMinutes = $defaultCheckInLengthInMinutes", - "cryptographicSeed = $cryptographicSeed", - "cnPublicKey = $cnPublicKey" - ).joinToString(separator = "\n") - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt deleted file mode 100644 index 45e3c4763e4..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.showevents - -import dagger.Binds -import dagger.Module -import dagger.multibindings.IntoMap -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory -import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey - -@Module -abstract class ShowStoredEventsTestFragmentModule { - - @Binds - @IntoMap - @CWAViewModelKey(ShowStoredEventsTestViewModel::class) - abstract fun testStoredEventsFragment( - factory: ShowStoredEventsTestViewModel.Factory - ): CWAViewModelFactory -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt deleted file mode 100644 index f85513da0a4..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.showevents - -import androidx.lifecycle.asLiveData -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory - -class ShowStoredEventsTestViewModel @AssistedInject constructor( - dispatcherProvider: DispatcherProvider, - private val traceLocationRepository: TraceLocationRepository -) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - - @AssistedFactory - interface Factory : SimpleCWAViewModelFactory - - val storedEvents = traceLocationRepository.allTraceLocations.asLiveData() - - fun deleteAllEvents() { - traceLocationRepository.deleteAllTraceLocations() - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt index 4010a857cd5..9728bc8fd07 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -16,12 +16,6 @@ import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaOnboardingFragmentModul import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragmentModule -import de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragment -import de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragmentModule -import de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragment -import de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragmentModule -import de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragment -import de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragmentModule import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment @@ -79,15 +73,6 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [EventRegistrationTestFragmentModule::class]) abstract fun eventRegistration(): EventRegistrationTestFragment - @ContributesAndroidInjector(modules = [QrCodeCreationTestFragmentModule::class]) - abstract fun qrCodeCreation(): QrCodeCreationTestFragment - - @ContributesAndroidInjector(modules = [CreateEventTestFragmentModule::class]) - abstract fun createEvent(): CreateEventTestFragment - - @ContributesAndroidInjector(modules = [ShowStoredEventsTestFragmentModule::class]) - abstract fun showStoredEvents(): ShowStoredEventsTestFragment - @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class]) abstract fun showEventDetail(): QrCodeDetailFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml index d35124a53e2..1f01bcef3f5 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -1,8 +1,8 @@ - @@ -12,49 +12,6 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - - - - - - - - - - - - - - - + android:textIsSelectable="true" + tools:text="Location" /> + android:textIsSelectable="true" + tools:text="URL" /> + android:textIsSelectable="true" + tools:text="ID" /> + + + android:textIsSelectable="true" + tools:text="Location" /> + android:textIsSelectable="true" + tools:text="URL" /> - - - - - - - - - - - - - - - + tools:text="ID" /> + diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml deleted file mode 100644 index 7f8e4b10c71..00000000000 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml index 09b620c70da..1586ca6972c 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml @@ -133,40 +133,17 @@ android:label="EventRegistrationTestFragment" tools:layout="@layout/fragment_test_eventregistration"> - - - - + android:id="@+id/action_eventRegistrationTestFragment_to_qrCodePosterFragmentTest" + app:destination="@id/qrCodePosterFragmentTest" /> - - - - - + android:id="@+id/qrCodePosterFragmentTest" + android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment" + android:label="qr_code_poster_fragment" + tools:layout="@layout/qr_code_poster_fragment"> + + diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt index e68f689f30b..9e987ba5e53 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt @@ -23,7 +23,7 @@ class QrCodePosterTemplateModule : BaseEnvironmentModule() { @QrCodePosterTemplate fun cacheDir( @AppContext context: Context - ): File = File(context.cacheDir, "qrCodePoster") + ): File = File(context.cacheDir, "poster") @Singleton @Provides diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt new file mode 100644 index 00000000000..f00a0c22f14 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt @@ -0,0 +1,67 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject +import kotlin.math.roundToInt + +class PosterTemplateProvider @Inject constructor( + private val posterTemplateServer: QrCodePosterTemplateServer, + private val dispatcherProvider: DispatcherProvider, + @AppContext private val context: Context +) { + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun template(): Template = withContext(dispatcherProvider.IO) { + val templateData = posterTemplateServer.downloadQrCodePosterTemplate() + val file = File(context.cacheDir, "template.pdf") + FileOutputStream(file).use { it.write(templateData.template.toByteArray()) } + + val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + val renderer = PdfRenderer(fileDescriptor) + + val page = renderer.openPage(0) + val scale = (context.resources.displayMetrics.density / page.width * page.height).roundToInt() + Timber.d("scale=$scale") + val bitmap = Bitmap.createBitmap( + context.resources.displayMetrics, + page.width * scale, + page.height * scale, + Bitmap.Config.ARGB_8888 + ) + + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + page.close() + renderer.close() + file.delete() + + Template( + bitmap = bitmap, + width = page.width, + height = page.height, + offsetX = templateData.offsetX, + offsetY = templateData.offsetY, + qrCodeLength = templateData.qrCodeSideLength, + textBox = templateData.descriptionTextBox + ) + } +} + +data class Template( + val bitmap: Bitmap?, + val width: Int, + val height: Int, + val offsetX: Float, + val offsetY: Float, + val qrCodeLength: Int, + val textBox: QRCodeTextBoxAndroid +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt index 6dac184d63c..96a244814db 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.ui.eventregistration.organizer.details +package de.rki.coronawarnapp.eventregistration.checkins.qrcode import android.content.Context import android.graphics.Bitmap @@ -6,6 +6,7 @@ import android.graphics.Color import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import dagger.Reusable import de.rki.coronawarnapp.appconfig.AppConfigProvider @@ -19,22 +20,31 @@ class QrCodeGenerator @Inject constructor( @AppContext private val context: Context, ) { - suspend fun createQrCode(input: String, size: Int = 1000): Bitmap? { - - val qrCodeErrorCorrectionLevel = appConfigProvider + /** + * Decodes input String into a QR Code [Bitmap] + * @param input [String] + * @param length [Int] QR Code side length + * @param margin [Int] QR Code side's margin + * + * @throws [Exception] it could throw [IllegalArgumentException] , [WriterException] + * or exception while creating the bitmap + */ + suspend fun createQrCode(input: String, length: Int = 1000, margin: Int = 1): Bitmap { + val correctionLevel = appConfigProvider .getAppConfig() .presenceTracing .qrCodeErrorCorrectionLevel - Timber.i("QrCodeErrorCorrectionLevel: $qrCodeErrorCorrectionLevel") + Timber.i("correctionLevel=$correctionLevel") + val hints = mapOf( - EncodeHintType.ERROR_CORRECTION to qrCodeErrorCorrectionLevel + EncodeHintType.ERROR_CORRECTION to correctionLevel, + EncodeHintType.MARGIN to margin ) - return MultiFormatWriter().encode( input, BarcodeFormat.QR_CODE, - size, - size, + length, + length, hints ).toBitmap() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt index 0669968f006..3bdc275bc76 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt @@ -4,7 +4,6 @@ import android.os.Parcelable import com.google.common.io.BaseEncoding import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass -import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeGenerator import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import okio.ByteString diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt new file mode 100644 index 00000000000..3ae8e15318e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.ui.color + +import android.graphics.Color +import androidx.annotation.ColorInt +import timber.log.Timber + +/** + * Parse color from String - default color is a fallback + * @param defaultColor [Int] color + */ +fun String.parseColor(@ColorInt defaultColor: Int = Color.BLACK): Int = + try { + Color.parseColor(this) + } catch (e: Exception) { + Timber.d(e, "Parsing color failed") + defaultColor + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt index 8840ff0adfe..cd6d537b10a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -20,6 +20,8 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFr import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment +import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragmentModule @Module internal abstract class EventRegistrationUIModule { @@ -50,4 +52,7 @@ internal abstract class EventRegistrationUIModule { @ContributesAndroidInjector(modules = [TraceLocationsFragmentModule::class]) abstract fun traceLocationsFragment(): TraceLocationsFragment + + @ContributesAndroidInjector(modules = [QrCodePosterFragmentModule::class]) + abstract fun qrCodePosterFragment(): QrCodePosterFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt index a300e882482..bd2763bc00c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt @@ -11,10 +11,12 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener +import com.google.android.material.transition.MaterialContainerTransform import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationOrganizerQrCodeDetailFragmentBinding import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack import de.rki.coronawarnapp.util.ui.viewBindingLazy @@ -29,7 +31,7 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ private val navArgs by navArgs() - private val vm: QrCodeDetailViewModel by cwaViewModelsAssisted( + private val viewModel: QrCodeDetailViewModel by cwaViewModelsAssisted( factoryProducer = { viewModelFactory }, constructorCall = { factory, _ -> factory as QrCodeDetailViewModel.Factory @@ -39,6 +41,13 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ private val binding: TraceLocationOrganizerQrCodeDetailFragmentBinding by viewBindingLazy() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedElementEnterTransition = MaterialContainerTransform() + sharedElementReturnTransition = MaterialContainerTransform() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -59,11 +68,16 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ toolbar.apply { navigationIcon = context.getDrawableCompat(R.drawable.ic_close_white) navigationContentDescription = getString(R.string.accessibility_close) - setNavigationOnClickListener { vm.onBackButtonPress() } + setNavigationOnClickListener { viewModel.onBackButtonPress() } + } + + qrCodePrintButton.setOnClickListener { + viewModel.onPrintQrCode() } } - vm.qrCodeBitmap.observe2(this) { + viewModel.qrCodeBitmap.observe2(this) { + binding.progressBar.hide() binding.qrCodeImage.apply { val resourceId = RoundedBitmapDrawableFactory.create(resources, it) resourceId.cornerRadius = it.width * 0.1f @@ -71,19 +85,20 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ } } - vm.routeToScreen.observe2(this) { + viewModel.routeToScreen.observe2(this) { when (it) { - QrCodeDetailNavigationEvents.NavigateBack -> { - popBackStack() - } - QrCodeDetailNavigationEvents.NavigateToPrintFragment -> { /* TODO */ - } + QrCodeDetailNavigationEvents.NavigateBack -> popBackStack() + QrCodeDetailNavigationEvents.NavigateToDuplicateFragment -> { /* TODO */ } + + is QrCodeDetailNavigationEvents.NavigateToQrCodePosterFragment -> doNavigate( + QrCodeDetailFragmentDirections.actionQrCodeDetailFragmentToQrCodePosterFragment(it.locationId) + ) } } - vm.uiState.observe2(this) { uiState -> + viewModel.uiState.observe2(this) { uiState -> with(binding) { title.text = uiState.description subtitle.text = uiState.address diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt index b91b99b27a5..68027a1fd55 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt @@ -2,6 +2,6 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.details sealed class QrCodeDetailNavigationEvents { object NavigateBack : QrCodeDetailNavigationEvents() - object NavigateToPrintFragment : QrCodeDetailNavigationEvents() + data class NavigateToQrCodePosterFragment(val locationId: Long) : QrCodeDetailNavigationEvents() object NavigateToDuplicateFragment : QrCodeDetailNavigationEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt index 863daf7a12b..792ff9e53e9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt @@ -3,10 +3,15 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.details import android.graphics.Bitmap import androidx.lifecycle.asLiveData import dagger.assisted.Assisted +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -18,7 +23,7 @@ import org.joda.time.Instant import timber.log.Timber class QrCodeDetailViewModel @AssistedInject constructor( - @Assisted private val traceLocationId: Long?, + @Assisted private val traceLocationId: Long, private val dispatcher: DispatcherProvider, private val qrCodeGenerator: QrCodeGenerator, private val traceLocationRepository: DefaultTraceLocationRepository @@ -29,11 +34,12 @@ class QrCodeDetailViewModel @AssistedInject constructor( private val subtitleFlow = MutableStateFlow(null) private val startTimeFlow = MutableStateFlow(null) private val endTimeFlow = MutableStateFlow(null) + private val bitmapLiveData = MutableLiveData() init { launch { - val traceLocation = traceLocationRepository.traceLocationForId(traceLocationId ?: 0L) + val traceLocation = traceLocationRepository.traceLocationForId(traceLocationId) if (titleFlow.value == null) { titleFlow.value = traceLocation.description @@ -80,8 +86,7 @@ class QrCodeDetailViewModel @AssistedInject constructor( val endDateTime: Instant? get() = endInstant } - val qrCodeBitmap = SingleLiveEvent() - val errorMessage = SingleLiveEvent() + val qrCodeBitmap: LiveData = bitmapLiveData val routeToScreen: SingleLiveEvent = SingleLiveEvent() /** @@ -91,10 +96,10 @@ class QrCodeDetailViewModel @AssistedInject constructor( try { val input = traceLocation.locationUrl Timber.d("input=$input") - qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input)) + bitmapLiveData.postValue(qrCodeGenerator.createQrCode(input)) } catch (e: Exception) { Timber.d(e, "Qr code creation failed") - errorMessage.postValue(e.localizedMessage ?: "QR code creation failed") + e.report(ExceptionCategory.INTERNAL) } } @@ -102,10 +107,16 @@ class QrCodeDetailViewModel @AssistedInject constructor( routeToScreen.postValue(QrCodeDetailNavigationEvents.NavigateBack) } + fun onPrintQrCode() { + routeToScreen.postValue( + QrCodeDetailNavigationEvents.NavigateToQrCodePosterFragment(traceLocationId) + ) + } + @AssistedFactory interface Factory : CWAViewModelFactory { fun create( - traceLocationId: Long? + traceLocationId: Long ): QrCodeDetailViewModel } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt index d66aa636121..5909c982933 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt @@ -10,5 +10,7 @@ sealed class TraceLocationEvent { data class ConfirmSwipeItem(val traceLocation: TraceLocation, val position: Int) : TraceLocationEvent() - data class StartQrCodeDetailFragment(val id: Long) : TraceLocationEvent() + data class StartQrCodeDetailFragment(val id: Long, val position: Int) : TraceLocationEvent() + + data class StartQrCodePosterFragment(val traceLocation: TraceLocation) : TraceLocationEvent() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt index f9edf14cac9..803b8719aea 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt @@ -14,6 +14,7 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationOrganizerTraceLocationsListFragmentBinding import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.traceLocationCategories +import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragmentArgs import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.list.isSwipeable @@ -81,22 +82,36 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ showDeleteSingleDialog(it.traceLocation, it.position) } is TraceLocationEvent.StartQrCodeDetailFragment -> { - doNavigate( - TraceLocationsFragmentDirections.actionTraceLocationOrganizerListFragmentToQrCodeDetailFragment( - traceLocationId = it.id, - ) + + val navigatorExtras = binding.recyclerView.findViewHolderForAdapterPosition(it.position)?.itemView + ?.run { + // Set it on the fly to avoid confusion of recycler's items + this.transitionName = "trace_location_container_transition" + FragmentNavigatorExtras(this to this.transitionName) + } + + findNavController().navigate( + R.id.action_traceLocationsFragment_to_qrCodeDetailFragment, + QrCodeDetailFragmentArgs(traceLocationId = it.id).toBundle(), + null, + navigatorExtras ) } is TraceLocationEvent.DuplicateItem -> { openCreateEventFragment(it.traceLocation) } + is TraceLocationEvent.StartQrCodePosterFragment -> doNavigate( + TraceLocationsFragmentDirections.actionTraceLocationsFragmentToQrCodePosterFragment( + it.traceLocation.id + ) + ) } } binding.qrCodeFab.apply { setOnClickListener { findNavController().navigate( - R.id.action_traceLocationOrganizerListFragment_to_traceLocationOrganizerCategoriesFragment, + R.id.action_traceLocationsFragment_to_traceLocationCategoryFragment, null, null, FragmentNavigatorExtras(this to transitionName) @@ -116,7 +131,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ when (it.itemId) { R.id.menu_information -> { findNavController().navigate( - R.id.action_traceLocationOrganizerListFragment_to_traceLocationOrganizerQRInfoFragment + R.id.action_traceLocationOrganizerListFragment_to_traceLocationInfoFragment ) true } @@ -149,11 +164,10 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ Timber.e("Category not found, traceLocation = $traceLocation") } else { findNavController().navigate( - TraceLocationsFragmentDirections - .actionTraceLocationOrganizerListFragmentToTraceLocationCreateFragment( - category, - traceLocation - ) + TraceLocationsFragmentDirections.actionTraceLocationsFragmentToTraceLocationCreateFragment( + category, + traceLocation + ) ) } } @@ -166,18 +180,10 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ viewModel.deleteSingleTraceLocation(traceLocation) } setNegativeButton(R.string.trace_location_organiser_list_delete_all_popup_negative_button) { _, _ -> - position?.let { - traceLocationsAdapter.notifyItemChanged( - position - ) - } + position?.let { traceLocationsAdapter.notifyItemChanged(position) } } setOnCancelListener { - position?.let { - traceLocationsAdapter.notifyItemChanged( - position - ) - } + position?.let { traceLocationsAdapter.notifyItemChanged(position) } } }.show() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt index 8c57430618b..d4167ccd566 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt @@ -35,14 +35,18 @@ class TraceLocationsViewModel @AssistedInject constructor( traceLocation = traceLocation, onCheckIn = { /* TODO */ }, onDuplicate = { events.postValue(TraceLocationEvent.DuplicateItem(it)) }, - onShowPrint = { /* TODO */ }, - onClearItem = { events.postValue(TraceLocationEvent.ConfirmDeleteItem(it)) }, - onSwipeItem = { traceLocation, position -> + onShowPrint = { events.postValue(TraceLocationEvent.StartQrCodePosterFragment(it)) }, + onDeleteItem = { events.postValue(TraceLocationEvent.ConfirmDeleteItem(it)) }, + onSwipeItem = { location, position -> events.postValue( - TraceLocationEvent.ConfirmSwipeItem(traceLocation, position) + TraceLocationEvent.ConfirmSwipeItem(location, position) + ) + }, + onCardClicked = { traceLocation, position -> + events.postValue( + TraceLocationEvent.StartQrCodeDetailFragment(traceLocation.id, position) ) }, - onCardClicked = { events.postValue(TraceLocationEvent.StartQrCodeDetailFragment(it.id)) }, ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt index 8cc28cf70dd..da8cca4747f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt @@ -57,14 +57,13 @@ class TraceLocationVH(parent: ViewGroup) : when (it.itemId) { R.id.menu_duplicate -> item.onDuplicate(item.traceLocation).let { true } R.id.menu_show_print -> item.onShowPrint(item.traceLocation).let { true } - R.id.menu_clear -> item.onClearItem(item.traceLocation).let { true } + R.id.menu_clear -> item.onDeleteItem(item.traceLocation).let { true } else -> false } } checkinAction.setOnClickListener { item.onCheckIn(item.traceLocation) } - - itemView.setOnClickListener { item.onCardClicked(item.traceLocation) } + itemView.setOnClickListener { item.onCardClicked(item.traceLocation, adapterPosition) } } data class Item( @@ -72,9 +71,9 @@ class TraceLocationVH(parent: ViewGroup) : val onCheckIn: (TraceLocation) -> Unit, val onDuplicate: (TraceLocation) -> Unit, val onShowPrint: (TraceLocation) -> Unit, - val onClearItem: (TraceLocation) -> Unit, + val onDeleteItem: (TraceLocation) -> Unit, val onSwipeItem: (TraceLocation, Int) -> Unit, - val onCardClicked: (TraceLocation) -> Unit + val onCardClicked: (TraceLocation, Int) -> Unit ) : TraceLocationItem, SwipeConsumer { override val stableId: Long = traceLocation.id.hashCode().toLong() override fun onSwipe(position: Int, direction: Int) = onSwipeItem(traceLocation, position) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt new file mode 100644 index 00000000000..b5840444875 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt @@ -0,0 +1,156 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster + +import android.os.Bundle +import android.print.PrintAttributes +import android.print.PrintManager +import android.util.TypedValue +import android.view.View +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.getSystemService +import androidx.core.widget.TextViewCompat +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.QrCodePosterFragmentBinding +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid +import de.rki.coronawarnapp.ui.color.parseColor +import de.rki.coronawarnapp.ui.print.PrintingAdapter +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.files.FileSharing +import de.rki.coronawarnapp.util.ui.popBackStack +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 java.io.File +import javax.inject.Inject + +class QrCodePosterFragment : Fragment(R.layout.qr_code_poster_fragment), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val args by navArgs() + private val viewModel: QrCodePosterViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as QrCodePosterViewModel.Factory + factory.create(args.traceLocationId) + } + ) + + private val binding: QrCodePosterFragmentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + toolbar.setNavigationOnClickListener { popBackStack() } + viewModel.poster.observe(viewLifecycleOwner) { poster -> + bindPoster(poster) + // Avoid creating blank PDF + if (poster.hasImages()) onPosterDrawn() + } + } + + viewModel.sharingIntent.observe(viewLifecycleOwner) { + onShareIntent(it) + } + } + + private fun QrCodePosterFragmentBinding.bindPoster(poster: Poster) { + Timber.d("poster=$poster") + progressBar.hide() + + val template = poster.template ?: return // Exit early + Timber.d("template=$template") + + // Adjust poster image dimensions ratio to have a proper printing preview + val posterLayoutParam = posterImage.layoutParams as ConstraintLayout.LayoutParams + val dimensionRatio = template.run { "$width:$height" } // W:H + Timber.d("dimensionRatio=$dimensionRatio") + posterLayoutParam.dimensionRatio = dimensionRatio + + // Display images + qrCodeImage.setImageBitmap(poster.qrCode) + posterImage.setImageBitmap(template.bitmap) + + // Position QR Code image based on data provided by server + topGuideline.setGuidelinePercent(template.offsetY) + startGuideline.setGuidelinePercent(template.offsetX) + endGuideline.setGuidelinePercent(1 - template.offsetX) + + // Bind text info + bindTextBox(poster.infoText, poster.template.textBox) + } + + private fun onPosterDrawn() = with(binding.qrCodePoster) { + viewTreeObserver.addOnGlobalLayoutListener( + object : OnGlobalLayoutListener { + override fun onGlobalLayout() { + viewModel.createPDF(binding.qrCodePoster) + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + } + ) + } + + private fun QrCodePosterFragmentBinding.bindTextBox( + infoText: String, + textBox: QRCodeTextBoxAndroid + ) = with(infoTextView) { + text = infoText + val minFontSize = textBox.fontSize - 6 + val maxFontSize = textBox.fontSize + TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration( + infoTextView, + minFontSize, + maxFontSize, + 1, + TypedValue.COMPLEX_UNIT_SP + ) + setTextSize(TypedValue.COMPLEX_UNIT_SP, maxFontSize.toFloat()) + setTextColor(textBox.fontColor.parseColor()) + textEndGuideline.setGuidelinePercent(1 - textBox.offsetX) + textStartGuideline.setGuidelinePercent(textBox.offsetX) + textTopGuideline.setGuidelinePercent(textBox.offsetY) + // TODO setTypeface() + } + + private fun onShareIntent(fileIntent: FileSharing.FileIntentProvider) { + binding.toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_print -> printFile(fileIntent.file).run { true } + R.id.action_share -> startActivity(fileIntent.intent(requireActivity())).run { true } + else -> false + } + } + } + + private fun printFile(file: File) { + val printingManger = context?.getSystemService() + Timber.i("PrintingManager=$printingManger") + if (printingManger == null) { + Toast.makeText(requireContext(), R.string.errors_generic_headline, Toast.LENGTH_LONG).show() + return + } + + try { + val job = printingManger.print( + getString(R.string.app_name), + PrintingAdapter(file), + PrintAttributes.Builder() + .setMediaSize(PrintAttributes.MediaSize.ISO_A3) + .build() + ) + + Timber.d("JobState=%s", job.info.state) + } catch (e: Exception) { + Timber.d(e, "Printing job failed") + e.report(ExceptionCategory.INTERNAL) + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt similarity index 56% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt index 33fa4c48d9b..a64e78afba4 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.qrcode +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster import dagger.Binds import dagger.Module @@ -8,11 +8,11 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey @Module -abstract class QrCodeCreationTestFragmentModule { +abstract class QrCodePosterFragmentModule { @Binds @IntoMap - @CWAViewModelKey(QrCodeCreationTestViewModel::class) - abstract fun qrCodeCreation( - factory: QrCodeCreationTestViewModel.Factory + @CWAViewModelKey(QrCodePosterViewModel::class) + abstract fun qrCodePosterFragment( + factory: QrCodePosterViewModel.Factory ): CWAViewModelFactory } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt new file mode 100644 index 00000000000..3a5a36a36e0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt @@ -0,0 +1,119 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster + +import android.graphics.Bitmap +import android.graphics.pdf.PdfDocument +import android.view.View +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.PosterTemplateProvider +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.Template +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.files.FileSharing +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.lang.ref.WeakReference + +class QrCodePosterViewModel @AssistedInject constructor( + @Assisted private val traceLocationId: Long, + private val dispatcher: DispatcherProvider, + private val qrCodeGenerator: QrCodeGenerator, + private val posterTemplateProvider: PosterTemplateProvider, + private val traceLocationRepository: TraceLocationRepository, + private val fileSharing: FileSharing +) : CWAViewModel(dispatcher) { + + private val posterLiveData = MutableLiveData() + val poster: LiveData = posterLiveData + val sharingIntent = SingleLiveEvent() + + init { + generatePoster() + } + + /** + * Create a new PDF file and result is delivered by [sharingIntent] + * as a sharing [FileSharing.ShareIntentProvider] + */ + @Suppress("BlockingMethodInNonBlockingContext") + fun createPDF(view: View) = launch(context = dispatcher.IO) { + try { + val weakViewRef = WeakReference(view) // Accessing view in background thread + val directory = File(view.context.cacheDir, "poster").apply { if (!exists()) mkdirs() } + val file = File(directory, "cwa-qr-code.pdf") + + val weakView = weakViewRef.get() ?: return@launch // View is not existing anymore + val pageInfo = PdfDocument.PageInfo.Builder(weakView.width, weakView.height, 1).create() + + PdfDocument().apply { + startPage(pageInfo).apply { + weakView.draw(canvas) + finishPage(this) + } + + FileOutputStream(file).use { + writeTo(it) + close() + } + } + + sharingIntent.postValue(fileSharing.getFileIntentProvider(file, traceLocation().description)) + } catch (e: Exception) { + Timber.d(e, "Creating pdf failed") + e.report(ExceptionCategory.INTERNAL) + } + } + + private fun generatePoster() = launch(context = dispatcher.IO) { + try { + val traceLocation = traceLocation() + val template = posterTemplateProvider.template() + Timber.d("template=$template") + val qrCode = qrCodeGenerator.createQrCode( + input = traceLocation.locationUrl, + length = template.qrCodeLength, + margin = 0 + ) + + val textInfo = buildString { + append(traceLocation.description) + appendLine() + append(traceLocation.address) + } + posterLiveData.postValue( + Poster(qrCode, template, textInfo) + ) + } catch (e: Exception) { + Timber.d(e, "Generating poster failed") + posterLiveData.postValue(Poster()) + e.report(ExceptionCategory.INTERNAL) + } + } + + private suspend fun traceLocation() = traceLocationRepository.traceLocationForId(traceLocationId) + + @AssistedFactory + interface Factory : CWAViewModelFactory { + fun create( + traceLocationId: Long + ): QrCodePosterViewModel + } +} + +data class Poster( + val qrCode: Bitmap? = null, + val template: Template? = null, + val infoText: String = "" +) { + fun hasImages(): Boolean = qrCode != null && template?.bitmap != null +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt index a51ff449508..70500932453 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -35,7 +35,7 @@ import javax.inject.Inject class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val vm: HomeFragmentViewModel by cwaViewModels( + private val viewModel: HomeFragmentViewModel by cwaViewModels( ownerProducer = { requireActivity().viewModelStore }, factoryProducer = { viewModelFactory } ) @@ -53,7 +53,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { homeMenu.setupMenu(binding.toolbar) - vm.tracingHeaderState.observe2(this) { + viewModel.tracingHeaderState.observe2(this) { binding.tracingHeader = it } @@ -64,11 +64,11 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { adapter = homeAdapter } - vm.homeItems.observe2(this) { + viewModel.homeItems.observe2(this) { homeAdapter.update(it) } - vm.routeToScreen.observe2(this) { + viewModel.routeToScreen.observe2(this) { doNavigate(it) } @@ -76,28 +76,28 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsTracingFragment()) } - vm.openFAQUrlEvent.observe2(this) { + viewModel.openFAQUrlEvent.observe2(this) { ExternalActionHelper.openUrl(this@HomeFragment, getString(R.string.main_about_link)) } - vm.openTraceLocationOrganizerFlow.observe2(this) { - vm.wasQRInfoWasAcknowledged() + viewModel.openTraceLocationOrganizerFlow.observe2(this) { + viewModel.wasQRInfoWasAcknowledged() val nestedGraph = findNavController().graph.findNode(R.id.trace_location_organizer_nav_graph) as NavGraph - if (vm.wasQRInfoWasAcknowledged()) { - nestedGraph.startDestination = R.id.traceLocationOrganizerListFragment + if (viewModel.wasQRInfoWasAcknowledged()) { + nestedGraph.startDestination = R.id.traceLocationsFragment } else { - nestedGraph.startDestination = R.id.traceLocationOrganizerQRInfoFragment + nestedGraph.startDestination = R.id.traceLocationInfoFragment } doNavigate(HomeFragmentDirections.actionMainFragmentToTraceLocationOrganizerNavGraph()) } - vm.popupEvents.observe2(this) { event -> + viewModel.popupEvents.observe2(this) { event -> when (event) { HomeFragmentEvents.ShowErrorResetDialog -> { RecoveryByResetDialogFactory(this).showDialog( detailsLink = R.string.errors_generic_text_catastrophic_error_encryption_failure, - onPositive = { vm.errorResetDialogDismissed() } + onPositive = { viewModel.errorResetDialogDismissed() } ) } HomeFragmentEvents.ShowDeleteTestDialog -> showRemoveTestDialog() @@ -109,29 +109,29 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { } HomeFragmentEvents.ShowTracingExplanation -> { tracingExplanationDialog.show { - vm.tracingExplanationWasShown() + viewModel.tracingExplanationWasShown() } } } } - vm.showPopUps() + viewModel.showPopUps() - vm.showLoweredRiskLevelDialog.observe2(this) { + viewModel.showLoweredRiskLevelDialog.observe2(this) { if (it) showRiskLevelLoweredDialog() } - vm.showIncorrectDeviceTimeDialog.observe2(this) { showDialog -> + viewModel.showIncorrectDeviceTimeDialog.observe2(this) { showDialog -> if (!showDialog) return@observe2 - deviceTimeIncorrectDialog.show { vm.userHasAcknowledgedIncorrectDeviceTime() } + deviceTimeIncorrectDialog.show { viewModel.userHasAcknowledgedIncorrectDeviceTime() } } - vm.observeTestResultToSchedulePositiveTestResultReminder() + viewModel.observeTestResultToSchedulePositiveTestResultReminder() } override fun onResume() { super.onResume() - vm.refreshRequiredData() - vm.restoreAppShortcuts() + viewModel.refreshRequiredData() + viewModel.restoreAppShortcuts() binding.container.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) } @@ -143,7 +143,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { R.string.submission_test_result_dialog_remove_test_button_positive, R.string.submission_test_result_dialog_remove_test_button_negative, positiveButtonFunction = { - vm.deregisterWarningAccepted() + viewModel.deregisterWarningAccepted() } ) DialogHelper.showDialog(removeTestDialog).apply { @@ -160,7 +160,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { R.string.dialog_reactivate_risk_calculation_button_positive, R.string.dialog_reactivate_risk_calculation_button_negative, positiveButtonFunction = { - vm.reenableRiskCalculation() + viewModel.reenableRiskCalculation() } ) DialogHelper.showDialog(removeTestDialog).apply { @@ -177,7 +177,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { positiveButton = R.string.risk_lowered_dialog_button_confirm, negativeButton = null, cancelable = false, - positiveButtonFunction = { vm.userHasAcknowledgedTheLoweredRiskLevel() } + positiveButtonFunction = { viewModel.userHasAcknowledgedTheLoweredRiskLevel() } ) DialogHelper.showDialog(riskLevelLoweredDialog).apply { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt similarity index 97% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt index 2d20c87a1d4..622afae9fb3 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui +package de.rki.coronawarnapp.ui.print import android.os.Bundle import android.os.CancellationSignal diff --git a/Corona-Warn-App/src/main/res/drawable/ic_print.xml b/Corona-Warn-App/src/main/res/drawable/ic_print.xml new file mode 100644 index 00000000000..1f420bdc37e --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_print.xml @@ -0,0 +1,9 @@ + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_share.xml b/Corona-Warn-App/src/main/res/drawable/ic_share.xml new file mode 100644 index 00000000000..2fde222268b --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml new file mode 100644 index 00000000000..f2b6d1d139a --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml index 86d8a6257e9..58782363908 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml @@ -4,9 +4,10 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/content_container" android:layout_width="match_parent" - android:contentDescription="@string/trace_location_event_detail_title_accessibility" + android:layout_height="match_parent" android:background="@drawable/trace_location_gradient_background" - android:layout_height="match_parent"> + android:contentDescription="@string/trace_location_event_detail_title_accessibility" + android:transitionName="trace_location_container_transition"> + tools:src="@drawable/ic_qrcode" + tools:tint="@android:color/black" /> + + diff --git a/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml b/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml new file mode 100644 index 00000000000..0158331ca36 --- /dev/null +++ b/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml index a80ae6d0584..aa52e6cb304 100644 --- a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml @@ -3,10 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/trace_location_organizer_nav_graph" - app:startDestination="@id/traceLocationOrganizerQRInfoFragment"> + app:startDestination="@id/traceLocationInfoFragment"> @@ -14,13 +14,13 @@ @@ -43,27 +43,33 @@ app:nullable="true" /> + android:id="@+id/action_traceLocationsFragment_to_traceLocationCategoryFragment" + app:destination="@id/traceLocationCategoryFragment" /> + + + android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationInfoFragment" + app:destination="@id/traceLocationInfoFragment" /> + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index c0ea1b0f915..5e76c5740c2 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -1968,4 +1968,10 @@ Einstellungen öffnen Drei Personen an einem Stehtisch, zwei von ihnen schauen auf ihr Smartphone. + + "Drucken" + + "Teilen" + + "Druckversion" diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index e369ddf69e9..ced53c0830f 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1977,4 +1977,10 @@ Einstellungen öffnen Drei Personen an einem Stehtisch, zwei von ihnen schauen auf ihr Smartphone. + + "Print" + + "Share" + + "Print version" diff --git a/Corona-Warn-App/src/main/res/xml/provider_paths.xml b/Corona-Warn-App/src/main/res/xml/provider_paths.xml index 104e4132425..823eb81820e 100644 --- a/Corona-Warn-App/src/main/res/xml/provider_paths.xml +++ b/Corona-Warn-App/src/main/res/xml/provider_paths.xml @@ -6,7 +6,8 @@ - + + \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt new file mode 100644 index 00000000000..78793ae2810 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt @@ -0,0 +1,77 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster + +import android.graphics.Bitmap +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.PosterTemplateProvider +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.Template +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid +import de.rki.coronawarnapp.util.files.FileSharing +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class QrCodePosterViewModelTest : BaseTest() { + + @MockK lateinit var qrCodeGenerator: QrCodeGenerator + @MockK lateinit var posterTemplateProvider: PosterTemplateProvider + @MockK lateinit var traceLocationRepository: TraceLocationRepository + @MockK lateinit var fileSharing: FileSharing + @MockK lateinit var qrCodeBitmap: Bitmap + @MockK lateinit var templateBitmap: Bitmap + @MockK lateinit var textBox: QRCodeTextBoxAndroid + @MockK lateinit var traceLocation: TraceLocation + private lateinit var template: Template + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + template = Template( + bitmap = templateBitmap, + width = 500, + height = 600, + offsetX = 0.15f, + offsetY = 0.14f, + qrCodeLength = 500, + textBox = textBox + ) + + coEvery { qrCodeGenerator.createQrCode("locationUrl", any(), any()) } returns qrCodeBitmap + coEvery { posterTemplateProvider.template() } returns template + coEvery { traceLocationRepository.traceLocationForId(any()) } returns traceLocation.apply { + every { description } returns "description" + every { address } returns "address" + every { locationUrl } returns "locationUrl" + } + } + + @Test + fun `Poster is requested in init`() { + createInstance().poster.getOrAwaitValue() shouldBe Poster( + qrCode = qrCodeBitmap, + template = template, + infoText = "description\naddress" + ) + } + + private fun createInstance() = QrCodePosterViewModel( + traceLocationId = 1, + dispatcher = TestDispatcherProvider(), + qrCodeGenerator = qrCodeGenerator, + posterTemplateProvider = posterTemplateProvider, + traceLocationRepository = traceLocationRepository, + fileSharing = fileSharing + ) +} diff --git a/build.gradle b/build.gradle index 533ae7cd654..271da8127e5 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { // Can be upgraded once new Android version is released or // the specific version is available via maven classpath 'com.android.tools:r8:2.0.88' - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion"