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

Survey debug menu (EXPOSUREAPP-4855) #2341

Merged
merged 14 commits into from
Feb 12, 2021
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ package de.rki.coronawarnapp.test.datadonation.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.core.app.ShareCompat
import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.fragment.app.Fragment
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.FragmentTestDatadonationBinding
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException
import de.rki.coronawarnapp.datadonation.survey.SurveyException
import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
import de.rki.coronawarnapp.util.DialogHelper
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.ui.observe2
import de.rki.coronawarnapp.util.ui.viewBindingLazy
Expand Down Expand Up @@ -71,6 +79,84 @@ class DataDonationTestFragment : Fragment(R.layout.fragment_test_datadonation),
vm.errorEvents.observe2(this) {
Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show()
}

binding.oneTimePasswordBody.text = vm.otp

vm.surveyConfig.observe2(this) {
binding.surveyConfigBody.text = it
}

vm.showErrorDialog.observe2(this) {
showErrorDialog(it)
}

vm.currentSafetyNetExceptionType.observe2(this) { type ->
binding.apply {
if (safetynetExceptionSimulationRadioGroup.childCount != SafetyNetException.Type.values().size) {
SafetyNetException.Type.values()
.forEach { safetynetExceptionSimulationRadioGroup.addRadioButton(it.name) }
}
safetynetExceptionSimulationRadioGroup.children.checkByName(type.name)
}
}

binding.apply {
safetynetExceptionSimulationRadioGroup.setOnCheckedChangeListener { group, checkedId ->
val rb = group.findViewById(checkedId) as RadioButton
if (!rb.isPressed) return@setOnCheckedChangeListener
vm.selectSafetyNetExceptionType(SafetyNetException.Type.valueOf(rb.text as String))
}

safetynetExceptionSimulationButton.setOnClickListener { vm.showSafetyNetErrorDialog() }
}

vm.currentSurveyExceptionType.observe2(this) { type ->
binding.apply {
if (surveyExceptionSimulationRadioGroup.childCount != SurveyException.Type.values().size) {
SurveyException.Type.values()
.forEach { surveyExceptionSimulationRadioGroup.addRadioButton(it.name) }
}
surveyExceptionSimulationRadioGroup.children.checkByName(type.name)
}
}

binding.apply {
surveyExceptionSimulationRadioGroup.setOnCheckedChangeListener { group, checkedId ->
val rb = group.findViewById(checkedId) as RadioButton
if (!rb.isPressed) return@setOnCheckedChangeListener
vm.selectSurveyExceptionType(SurveyException.Type.valueOf(rb.text as String))
}

surveyExceptionSimulationButton.setOnClickListener { vm.showSurveyErrorDialog() }
}
}

private fun RadioGroup.addRadioButton(text: String) {
val rb = RadioButton(context).apply {
this.text = text
id = ViewCompat.generateViewId()
}
addView(rb)
}

private fun Sequence<View>.checkByName(name: String) {
forEach {
it as RadioButton
it.isChecked = it.text == name
}
}

private fun showErrorDialog(@StringRes stringRes: Int) {
context?.let {
val dialog = DialogHelper.DialogInstance(
context = it,
title = R.string.datadonation_details_survey_consent_error_dialog_title,
message = stringRes,
positiveButton = R.string.datadonation_details_survey_consent_error_dialog_pos_button,
cancelable = false
)
DialogHelper.showDialog(dialog)
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
package de.rki.coronawarnapp.test.datadonation.ui

import androidx.annotation.StringRes
import androidx.lifecycle.asLiveData
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer
import de.rki.coronawarnapp.datadonation.safetynet.CWASafetyNet
import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetClientWrapper
import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException
import de.rki.coronawarnapp.datadonation.storage.OTPRepository
import de.rki.coronawarnapp.datadonation.survey.SurveyException
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import timber.log.Timber
import java.security.SecureRandom

class DataDonationTestFragmentViewModel @AssistedInject constructor(
dispatcherProvider: DispatcherProvider,
private val safetyNetClientWrapper: SafetyNetClientWrapper,
private val secureRandom: SecureRandom,
private val cwaSafetyNet: CWASafetyNet
private val cwaSafetyNet: CWASafetyNet,
otpRepository: OTPRepository,
appConfigProvider: AppConfigProvider
) : CWAViewModel(dispatcherProvider = dispatcherProvider) {

private val currentReportInternal = MutableStateFlow<SafetyNetClientWrapper.Report?>(null)
Expand All @@ -32,6 +41,21 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor(
val errorEvents = SingleLiveEvent<Throwable>()
val copyJWSEvent = SingleLiveEvent<String>()

val otp: String = otpRepository.otp?.toString() ?: "No OTP received yet"
BMItr marked this conversation as resolved.
Show resolved Hide resolved

val surveyConfig = appConfigProvider.currentConfig
.map { it.survey.toString() }
.asLiveData(context = dispatcherProvider.Default)

private val currentSafetyNetExceptionTypeInternal = MutableStateFlow(SafetyNetException.Type.values().first())
val currentSafetyNetExceptionType =
currentSafetyNetExceptionTypeInternal.asLiveData(context = dispatcherProvider.Default)

private val currentSurveyExceptionTypeInternal = MutableStateFlow(SurveyException.Type.values().first())
val currentSurveyExceptionType = currentSurveyExceptionTypeInternal.asLiveData(context = dispatcherProvider.Default)

val showErrorDialog = SingleLiveEvent<@StringRes Int>()

fun createSafetyNetReport() {
launch {
val nonce = ByteArray(16)
Expand Down Expand Up @@ -85,6 +109,42 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor(
}
}

fun selectSafetyNetExceptionType(type: SafetyNetException.Type) {
currentSafetyNetExceptionTypeInternal.value = type
}

fun showSafetyNetErrorDialog() {
when (currentSafetyNetExceptionTypeInternal.value) {
SafetyNetException.Type.APK_PACKAGE_NAME_MISMATCH,
BMItr marked this conversation as resolved.
Show resolved Hide resolved
SafetyNetException.Type.ATTESTATION_FAILED,
SafetyNetException.Type.ATTESTATION_REQUEST_FAILED,
SafetyNetException.Type.DEVICE_TIME_UNVERIFIED,
SafetyNetException.Type.NONCE_MISMATCH ->
R.string.datadonation_details_survey_consent_error_TRY_AGAIN_LATER
SafetyNetException.Type.BASIC_INTEGRITY_REQUIRED,
SafetyNetException.Type.CTS_PROFILE_MATCH_REQUIRED,
SafetyNetException.Type.EVALUATION_TYPE_BASIC_REQUIRED,
SafetyNetException.Type.EVALUATION_TYPE_HARDWARE_BACKED_REQUIRED ->
R.string.datadonation_details_survey_consent_error_DEVICE_NOT_TRUSTED
SafetyNetException.Type.DEVICE_TIME_INCORRECT ->
R.string.datadonation_details_survey_consent_error_CHANGE_DEVICE_TIME
SafetyNetException.Type.PLAY_SERVICES_VERSION_MISMATCH ->
R.string.datadonation_details_survey_consent_error_UPDATE_PLAY_SERVICES
SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED ->
R.string.datadonation_details_survey_consent_error_TIME_SINCE_ONBOARDING_UNVERIFIED
}.also { showErrorDialog.postValue(it) }
}

fun selectSurveyExceptionType(type: SurveyException.Type) {
currentSurveyExceptionTypeInternal.value = type
}

fun showSurveyErrorDialog() {
when (currentSurveyExceptionTypeInternal.value) {
SurveyException.Type.ALREADY_PARTICIPATED_THIS_MONTH -> R.string.datadonation_details_survey_consent_error_ALREADY_PARTICIPATED
}.also { showErrorDialog.postValue(it) }
}

@AssistedFactory
interface Factory : SimpleCWAViewModelFactory<DataDonationTestFragmentViewModel>
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
<TextView
android:id="@+id/safetynet_body"
android:layout_width="match_parent"
android:textIsSelectable="true"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_title"
Expand Down Expand Up @@ -86,9 +86,9 @@
<TextView
android:id="@+id/safetynet_requirements_body"
android:layout_width="match_parent"
android:textIsSelectable="true"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/safetynet_requirements_title"
Expand All @@ -115,6 +115,148 @@
app:layout_constraintTop_toBottomOf="@id/safetynet_requirements_body" />
</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/one_time_password_container"
style="@style/Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_tiny">

<TextView
android:id="@+id/one_time_password_title"
style="@style/headline6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="One Time Password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/one_time_password_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:textIsSelectable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/one_time_password_title"
tools:text="No OTP received yet" />
</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/survey_config_container"
style="@style/Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_tiny">

<TextView
android:id="@+id/survey_config_title"
style="@style/headline6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="Survey Config"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/survey_config_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:textIsSelectable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/survey_config_title"
tools:text="No survey config received yet" />
</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/safetynet_exception_simulation_container"
style="@style/Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_tiny">

<TextView
android:id="@+id/safetynet_exception_simulation_title"
style="@style/headline6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="SafetyNet Exception Simulation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<RadioGroup
android:id="@+id/safetynet_exception_simulation_radio_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/safetynet_exception_simulation_title" />

<Button
android:id="@+id/safetynet_exception_simulation_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:text="Show Error Dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/safetynet_exception_simulation_radio_group" />
</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/survey_exception_simulation_container"
style="@style/Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_tiny">

<TextView
android:id="@+id/survey_exception_simulation_title"
style="@style/headline6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="Survey Exception Simulation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<RadioGroup
android:id="@+id/survey_exception_simulation_radio_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/survey_exception_simulation_title" />

<Button
android:id="@+id/survey_exception_simulation_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_tiny"
android:text="Show Error Dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/survey_exception_simulation_radio_group" />
</androidx.constraintlayout.widget.ConstraintLayout>

</LinearLayout>
</androidx.core.widget.NestedScrollView>
</layout>