diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index e631969b4290fe76b22c48870f35bb55b96d1b9d..b4200bc33c96ba2d8132bc803a3657f3831a69f5 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -10,7 +10,7 @@ import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment -import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment +import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment @@ -36,7 +36,7 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { PlaygroundFragment.MENU_ITEM, DataDonationTestFragment.MENU_ITEM, DeltaonboardingFragment.MENU_ITEM, - EventRegistrationTestFragment.MENU_ITEM, + PresenceTracingTestFragment.MENU_ITEM, ).let { MutableLiveData(it) } } val showTestScreenEvent = SingleLiveEvent<TestMenuItem>() 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/presencetracing/ui/PresenceTracingTestFragment.kt similarity index 84% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt rename to Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt index 95d522e04234c83bbafcb5d659bf3b60cca610e0..8e54311d1f424a8bb319fe076fc00ba731524871 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/presencetracing/ui/PresenceTracingTestFragment.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui +package de.rki.coronawarnapp.test.presencetracing.ui import android.annotation.SuppressLint import android.os.Bundle @@ -11,7 +11,7 @@ import androidx.core.text.scale import androidx.core.view.isVisible import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding +import de.rki.coronawarnapp.databinding.FragmentTestPresenceTracingBinding import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat @@ -24,12 +24,12 @@ import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import javax.inject.Inject @SuppressLint("SetTextI18n") -class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregistration), AutoInject { +class PresenceTracingTestFragment : Fragment(R.layout.fragment_test_presence_tracing), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val viewModel: EventRegistrationTestFragmentViewModel by cwaViewModels { viewModelFactory } + private val viewModel: PresenceTracingTestViewModel by cwaViewModels { viewModelFactory } - private val binding: FragmentTestEventregistrationBinding by viewBindingLazy() + private val binding: FragmentTestPresenceTracingBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -71,8 +71,8 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis lastOrganiserLocationUrl.text = styleText("URL", traceLocation.locationUrl) qrcodeButton.setOnClickListener { doNavigate( - EventRegistrationTestFragmentDirections - .actionEventRegistrationTestFragmentToQrCodePosterFragmentTest(traceLocation.id) + PresenceTracingTestFragmentDirections + .actionPresenceTracingTestFragmentToQrCodePosterTestFragment(traceLocation.id) ) } } @@ -112,7 +112,7 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis buildSpannedString { bold { color(requireContext().getColorCompat(R.color.colorAccent)) { - append("$key=") + append("$key = ") } } @@ -121,14 +121,14 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis append(value.toString()) } } - append("\n") + appendLine() } companion object { val MENU_ITEM = TestMenuItem( - title = "Event Registration", - description = "View & Control the event registration.", - targetId = R.id.eventRegistrationTestFragment + title = "Presence Tracing", + description = "View & Control presence tracing", + targetId = R.id.presenceTracingTestFragment ) } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragmentModule.kt similarity index 53% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentModule.kt rename to Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragmentModule.kt index 3c95a0a4a47d729182eca300cc9e5b7a70ad655f..db41dfdf6a73e26b68393cb7cac9f3b978fba903 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragmentModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui +package de.rki.coronawarnapp.test.presencetracing.ui 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 EventRegistrationTestFragmentModule { +abstract class PresenceTracingTestFragmentModule { @Binds @IntoMap - @CWAViewModelKey(EventRegistrationTestFragmentViewModel::class) - abstract fun testEventRegistrationFragment( - factory: EventRegistrationTestFragmentViewModel.Factory + @CWAViewModelKey(PresenceTracingTestViewModel::class) + abstract fun testPresenceTracingFragment( + factory: PresenceTracingTestViewModel.Factory ): CWAViewModelFactory<out CWAViewModel> } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt similarity index 96% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt rename to Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt index 3913503fe7082eb9597ce0f5b8d136bd9d0513e1..7e0e674bcee1e3f48cb962d9a1f286e9ef20f8ba 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui +package de.rki.coronawarnapp.test.presencetracing.ui import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.map import timber.log.Timber import kotlin.system.measureTimeMillis -class EventRegistrationTestFragmentViewModel @AssistedInject constructor( +class PresenceTracingTestViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, traceLocationRepository: TraceLocationRepository, checkInRepository: CheckInRepository, @@ -144,5 +144,5 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory : SimpleCWAViewModelFactory<EventRegistrationTestFragmentViewModel> + interface Factory : SimpleCWAViewModelFactory<PresenceTracingTestViewModel> } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..c4aa4a257925c75f84e399058b78d5abfc9eddb6 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestFragment.kt @@ -0,0 +1,298 @@ +package de.rki.coronawarnapp.test.presencetracing.ui.poster + +import android.annotation.SuppressLint +import android.os.Bundle +import android.print.PrintAttributes +import android.print.PrintManager +import android.text.SpannedString +import android.util.TypedValue +import android.view.View +import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.getSystemService +import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.text.color +import androidx.core.view.isVisible +import androidx.core.widget.TextViewCompat +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestQrCodePosterBinding +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate +import de.rki.coronawarnapp.ui.color.parseColor +import de.rki.coronawarnapp.ui.print.PrintingAdapter +import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat +import de.rki.coronawarnapp.util.di.AutoInject +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 + +@SuppressLint("SetTextI18n") +class QrCodePosterTestFragment : Fragment(R.layout.fragment_test_qr_code_poster), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val args by navArgs<QrCodePosterTestFragmentArgs>() + private val viewModel: QrCodePosterTestViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as QrCodePosterTestViewModel.Factory + factory.create(args.traceLocationId) + } + ) + + private var itemId = -1 + + private val binding: FragmentTestQrCodePosterBinding 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) + bindToolbar() + } + } + + viewModel.sharingIntent.observe(viewLifecycleOwner) { fileIntent -> + when (itemId) { + R.id.action_print -> printFile(fileIntent.file) + R.id.action_share -> startActivity(fileIntent.intent(requireActivity())) + } + } + + viewModel.qrCodeBitmap.observe(viewLifecycleOwner) { + binding.qrCodeImage.setImageBitmap(it) + } + } + + private fun FragmentTestQrCodePosterBinding.bindPoster(poster: QrCodePosterTestViewModel.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) + + // Qr Code positioning + qrOffsetXSlider.apply { + value = template.offsetX.sliderValue + addOnChangeListener { _, value, fromUser -> + if (fromUser) { + val offset = value.percentage + startGuideline.setGuidelinePercent(offset) + endGuideline.setGuidelinePercent(1 - offset) + updateQrCodeOffsetText() + } + } + } + qrOffsetYSlider.apply { + value = template.offsetY.sliderValue + addOnChangeListener { _, value, fromUser -> + if (fromUser) { + topGuideline.setGuidelinePercent(value.percentage) + updateQrCodeOffsetText() + } + } + } + updateQrCodeOffsetText() + + qrLengthSlider.apply { + value = poster.template.qrCodeLength.toFloat() + addOnChangeListener { _, value, _ -> + updateQrCodeLengthText() + viewModel.generateQrCode(value.toInt()) + } + } + updateQrCodeLengthText() + + // Bind text info + bindTextBox(poster.infoText, poster.template.textBox) + offsetsPanel.isVisible = true + tooltip.setOnClickListener { + Toast.makeText(requireContext(), toastText(), Toast.LENGTH_LONG).show() + } + } + + private fun toastText(): SpannedString = + buildSpannedString { + bold { + append("Tips:") + } + color(requireContext().getColorCompat(R.color.colorAccent)) { + appendLine() + appendLine() + append( + "- Qr-Code Length defines Qr-Code bitmap length and not" + + "\nthe displayed Qr-Code ImageView where its length is flexible per screen size" + + "\nIn a way it defines the bitmap quality." + + "\nThe more the length the more the bitmaps's sharpness." + ) + + appendLine() + appendLine() + append( + "- Text below Qr-Code has max 2 lines and has a uniform auto scaling down to `fontSize - 6`." + + "\nIf the size is way larger than it fits in two lines, it will be cut." + ) + + appendLine() + appendLine() + append( + "- OffsetX defines the position of two guidelines left and right for the edges." + + "\n If the offset is increasing, the space in between the guidelines is decreasing." + + "\n Which means less width of the respective view. -> | -> view_width <- | <- " + ) + } + } + + private fun updateQrCodeLengthText() { + val value = binding.qrLengthSlider.value + binding.qrCodeLength.text = "Qr Code length:%d".format(value.toInt()) + } + + private fun FragmentTestQrCodePosterBinding.bindTextBox( + infoText: String, + textBox: QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid + ) = with(infoTextView) { + text = infoText + setFontSize(textBox.fontSize) + setTextColor(textBox.fontColor.parseColor()) + textEndGuideline.setGuidelinePercent(1 - textBox.offsetX) + textStartGuideline.setGuidelinePercent(textBox.offsetX) + textTopGuideline.setGuidelinePercent(textBox.offsetY) + + // Text Position + txtOffsetXSlider.apply { + value = textBox.offsetX.sliderValue + addOnChangeListener { _, value, fromUser -> + if (fromUser) { + val offset = value.percentage + textEndGuideline.setGuidelinePercent(1 - offset) + textStartGuideline.setGuidelinePercent(offset) + updateInfoOffsetText() + } + } + } + txtOffsetYSlider.apply { + value = textBox.offsetY.sliderValue + addOnChangeListener { _, value, fromUser -> + if (fromUser) { + textTopGuideline.setGuidelinePercent(value.percentage) + updateInfoOffsetText() + } + } + } + updateInfoOffsetText() + + // Text Size + infoTextSizeSlider.apply { + value = textBox.fontSize.toFloat() + addOnChangeListener { _, value, _ -> + updateFontSizeText() + setFontSize(value.toInt()) + } + } + updateFontSizeText() + + // Text Color + infoTextColorValue.doOnTextChanged { color, _, _, _ -> + infoTextView.setTextColor(color.toString().parseColor()) + } + } + + private fun updateFontSizeText() { + binding.infoTextSize.text = + "Font size: %s sp".format(binding.infoTextSizeSlider.value) + } + + private fun FragmentTestQrCodePosterBinding.setFontSize(maxFontSize: Int) { + val minFontSize = maxFontSize - 6 + TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration( + infoTextView, + minFontSize, + maxFontSize, + 1, + TypedValue.COMPLEX_UNIT_SP + ) + infoTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, maxFontSize.toFloat()) + } + + private fun FragmentTestQrCodePosterBinding.bindToolbar() { + toolbar.setOnMenuItemClickListener { + itemId = it.itemId + viewModel.createPDF(binding.qrCodePoster) + true + } + } + + private fun printFile(file: File) { + val printingManger = context?.getSystemService<PrintManager>() + 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) + } + } + + private fun updateQrCodeOffsetText() { + with(binding) { + qrCodeOffsets.text = "Qr Code offsets: X=%.3f, Y=%.3f".format( + qrOffsetXSlider.value.percentage, + qrOffsetYSlider.value.percentage + ) + } + } + + private fun updateInfoOffsetText() { + with(binding) { + infoTextOffsets.text = "Text offsets: X=%.3f, Y=%.3f".format( + txtOffsetXSlider.value.percentage, + txtOffsetYSlider.value.percentage + ) + } + } + + private val Float.percentage get() = this / 1000 + + private val Float.sliderValue get() = this * 1000 +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..871e9145d64a740fe20b77525813dc9174aca507 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.test.presencetracing.ui.poster + +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 QrCodePosterTestFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(QrCodePosterTestViewModel::class) + abstract fun qrCodePosterTestFragmentModule( + factory: QrCodePosterTestViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..f4cf799fa56161f0b5171622d89d8cca564f7e66 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/poster/QrCodePosterTestViewModel.kt @@ -0,0 +1,138 @@ +package de.rki.coronawarnapp.test.presencetracing.ui.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 QrCodePosterTestViewModel @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<Poster>() + val poster: LiveData<Poster> = posterLiveData + val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>() + val qrCodeBitmap = SingleLiveEvent<Bitmap>() + private var isRunning = false + + 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) + } + } + + fun generateQrCode(length: Int) = launch(context = dispatcher.IO) { + try { + if (isRunning) return@launch + isRunning = true + val traceLocation = traceLocation() + val qrCode = qrCodeGenerator.createQrCode( + input = traceLocation.locationUrl, + length = length, + margin = 0 + ) + qrCodeBitmap.postValue(qrCode) + } catch (e: Exception) { + Timber.e(e) + e.report(ExceptionCategory.INTERNAL) + } finally { + isRunning = false + } + } + + 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<QrCodePosterTestViewModel> { + fun create( + traceLocationId: Long + ): QrCodePosterTestViewModel + } + + data class Poster( + val qrCode: Bitmap? = null, + val template: Template? = null, + val infoText: String = "" + ) +} 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 9728bc8fd07c67ac0dec8dfe6185f066692b91e9..e4a3ea0afa6b77501ace2c87fe364335253996e6 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 @@ -14,22 +14,22 @@ import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragmentModule import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaOnboardingFragmentModule 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.presencetracing.ui.PresenceTracingTestFragment +import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragmentModule import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment import de.rki.coronawarnapp.test.menu.ui.TestMenuFragmentModule import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment import de.rki.coronawarnapp.test.playground.ui.PlaygroundModule +import de.rki.coronawarnapp.test.presencetracing.ui.poster.QrCodePosterTestFragment +import de.rki.coronawarnapp.test.presencetracing.ui.poster.QrCodePosterTestFragmentModule import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragmentModule import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragmentModule import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragmentModule -import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragment -import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragmentModule @Module abstract class MainActivityTestModule { @@ -70,9 +70,9 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [DeltaOnboardingFragmentModule::class]) abstract fun deltaOnboarding(): DeltaonboardingFragment - @ContributesAndroidInjector(modules = [EventRegistrationTestFragmentModule::class]) - abstract fun eventRegistration(): EventRegistrationTestFragment + @ContributesAndroidInjector(modules = [PresenceTracingTestFragmentModule::class]) + abstract fun eventRegistration(): PresenceTracingTestFragment - @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class]) - abstract fun showEventDetail(): QrCodeDetailFragment + @ContributesAndroidInjector(modules = [QrCodePosterTestFragmentModule::class]) + abstract fun showEventDetail(): QrCodePosterTestFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml similarity index 100% rename from Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml rename to Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml new file mode 100644 index 0000000000000000000000000000000000000000..590b0a48c28bc6f1771a2a1063778e0c93b67f41 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml @@ -0,0 +1,277 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="HardcodedText"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/white"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.BackArrow" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="2dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:menu="@menu/menu_trace_location_qr_code_poster" + app:title="@string/trace_location_organiser_poster_title" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_code_poster" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + + <ImageView + android:id="@+id/poster_image" + android:layout_width="0dp" + android:layout_height="0dp" + android:adjustViewBounds="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_constraintDimensionRatio="595:841" /> + + <ImageView + android:id="@+id/qr_code_image" + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="fitXY" + app:layout_constraintDimensionRatio="1:1" + app:layout_constraintEnd_toEndOf="@id/end_guideline" + app:layout_constraintStart_toStartOf="@id/start_guideline" + app:layout_constraintTop_toTopOf="@id/top_guideline" + tools:src="@drawable/ic_qrcode" + tools:tint="@android:color/black" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/start_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.16" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/end_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.84" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/top_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.095" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/text_start_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.132" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/text_end_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.87" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/text_top_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.61" /> + + <TextView + android:id="@+id/info_text_view" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:maxLines="2" + app:layout_constraintEnd_toEndOf="@id/text_end_guideline" + app:layout_constraintStart_toStartOf="@id/text_start_guideline" + app:layout_constraintTop_toTopOf="@id/text_top_guideline" + tools:ignore="SmallSp" + tools:text="Vereinsaktivität: Jahrestreffen der deutschen SAP Anwendergruppe\nHauptstr 3, 69115 Heidelberg" + tools:textColor="#000000" + tools:textSize="10sp" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/progress_bar" + android:layout_width="150dp" + android:layout_height="wrap_content" + android:indeterminate="true" + app:hideAnimationBehavior="inward" + app:indicatorColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + </androidx.constraintlayout.widget.ConstraintLayout> + + <ScrollView + android:id="@+id/offsets_panel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone" + app:behavior_peekHeight="60dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + <LinearLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:orientation="horizontal" + android:paddingBottom="20dp"> + + <ImageView + android:id="@+id/tooltip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center" + android:background="?selectableItemBackgroundBorderless" + app:srcCompat="@drawable/ic_info" /> + + <View + android:layout_width="100dp" + android:layout_height="5dp" + android:layout_gravity="center" + android:background="@color/colorAccent" /> + + </FrameLayout> + + <TextView + android:id="@+id/qr_code_offsets" + style="@style/body2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="QR Code offsets" /> + + <com.google.android.material.slider.Slider + android:id="@+id/qrOffsetXSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:stepSize="1" + android:valueFrom="0" + android:valueTo="1000" + app:labelBehavior="gone" /> + + <com.google.android.material.slider.Slider + android:id="@+id/qrOffsetYSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:stepSize="1" + android:valueFrom="0" + android:valueTo="1000" + app:labelBehavior="gone" /> + + <TextView + android:id="@+id/qr_code_length" + style="@style/body2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="Qr Code length" /> + + <com.google.android.material.slider.Slider + android:id="@+id/qrLengthSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:stepSize="100" + android:valueFrom="500" + android:valueTo="2000" + app:labelBehavior="gone" /> + + <View + android:layout_width="match_parent" + android:layout_height="5dp" + android:layout_marginVertical="10dp" + android:background="#BE818181" /> + + <TextView + android:id="@+id/info_text_offsets" + style="@style/body2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="Text offsets" /> + + <com.google.android.material.slider.Slider + android:id="@+id/txtOffsetXSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:stepSize="1" + android:valueFrom="0" + android:valueTo="1000" + app:labelBehavior="gone" /> + + <com.google.android.material.slider.Slider + android:id="@+id/txtOffsetYSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:stepSize="1" + android:valueFrom="0" + android:valueTo="1000" + app:labelBehavior="gone" /> + + <TextView + android:id="@+id/info_text_size" + style="@style/body2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="Font size" /> + + <com.google.android.material.slider.Slider + android:id="@+id/infoTextSizeSlider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:stepSize="1" + android:valueFrom="10" + android:valueTo="30" + app:labelBehavior="gone" /> + + <TextView + style="@style/body2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="Font Color" /> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/info_text_color_value" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="5dp" + android:hint="#000000 - FallbackColor=#000000" /> + </LinearLayout> + </ScrollView> +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file 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 1586ca6972c5d45da2f811603c2bce38e0fcf812..a8cdc52cfa9795a8acb3cced1acab554f81ed1d9 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 @@ -47,8 +47,8 @@ android:id="@+id/action_test_menu_fragment_to_deltaonboardingFragment" app:destination="@id/test_deltaonboarding_fragment" /> <action - android:id="@+id/action_test_menu_fragment_to_eventRegistrationTestFragment" - app:destination="@id/eventRegistrationTestFragment" /> + android:id="@+id/action_test_menu_fragment_to_presenceTracingTestFragment" + app:destination="@id/presenceTracingTestFragment" /> </fragment> <fragment @@ -128,19 +128,19 @@ android:label="DeltaonboardingFragment" tools:layout="@layout/fragment_test_deltaonboarding" /> <fragment - android:id="@+id/eventRegistrationTestFragment" - android:name="de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment" - android:label="EventRegistrationTestFragment" - tools:layout="@layout/fragment_test_eventregistration"> + android:id="@+id/presenceTracingTestFragment" + android:name="de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragment" + android:label="PresenceTracingTestFragment" + tools:layout="@layout/fragment_test_presence_tracing"> <action - android:id="@+id/action_eventRegistrationTestFragment_to_qrCodePosterFragmentTest" - app:destination="@id/qrCodePosterFragmentTest" /> + android:id="@+id/action_presenceTracingTestFragment_to_qrCodePosterTestFragment" + app:destination="@id/qrCodePosterTestFragment" /> </fragment> <fragment - 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"> + android:id="@+id/qrCodePosterTestFragment" + android:name="de.rki.coronawarnapp.test.presencetracing.ui.poster.QrCodePosterTestFragment" + android:label="QrCodePosterTestFragment" + tools:layout="@layout/fragment_test_qr_code_poster"> <argument android:name="traceLocationId" 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 e6f0123e69c4734517875010ea148e6006d78206..07b90907c3e3a2d6e3e9141c29603f5e9ba9f4fb 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 @@ -16,6 +16,8 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocatio import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragment +import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment @@ -55,4 +57,7 @@ internal abstract class EventRegistrationUIModule { @ContributesAndroidInjector(modules = [QrCodePosterFragmentModule::class]) abstract fun qrCodePosterFragment(): QrCodePosterFragment + + @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class]) + abstract fun showEventDetail(): QrCodeDetailFragment }