diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index aa533f1bc245557afe5139cf802e509b43525f98..334cbf113bc1c42348ed76a582f1ddacf28ddb5e 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -127,6 +127,10 @@ <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> + <option name="FIELD_ANNOTATION_WRAP" value="1" /> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + </indentOptions> </codeStyleSettings> </code_scheme> -</component> +</component> \ No newline at end of file diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index c4da9b2427af1bed9279c60ca405422493902501..63c99edc3af68ee60b7955e94f4c8376e34b8a96 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -137,6 +137,14 @@ android { jvmTarget = "1.8" } + def jvmCompilerArgs = ["-Xno-kotlin-nothing-value-exception"] + + tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile) { + kotlinOptions { + freeCompilerArgs = jvmCompilerArgs + } + } + lintOptions { checkAllWarnings = true } @@ -241,6 +249,9 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0' implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + + // DAGGER implementation 'com.google.dagger:dagger:2.28.1' implementation 'com.google.dagger:dagger-android:2.28.1' @@ -248,6 +259,8 @@ dependencies { kapt 'com.google.dagger:dagger-compiler:2.28.1' kapt 'com.google.dagger:dagger-android-processor:2.28.1' + compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2' + kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.2' // QR implementation('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false } diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..8cf4950155fce0e5dd3255ed9d5f8b1c7d511a4a --- /dev/null +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.ui.main + +import dagger.Module + +@Module +abstract class MainActivityTestModule { + + // This is an empty placeholder so we can use the same one, but not empty via deviceForTesters +} diff --git a/Corona-Warn-App/src/device/java/de.rki.coronawarnapp/ui/main/MainFragment.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/ui/main/MainFragment.kt similarity index 93% rename from Corona-Warn-App/src/device/java/de.rki.coronawarnapp/ui/main/MainFragment.kt rename to Corona-Warn-App/src/device/java/de/rki/coronawarnapp/ui/main/MainFragment.kt index 4b2300bc5c27b440054e145844cf415acfcb9370..a6a339419ee9e366314f67cad85d81345bf607a4 100644 --- a/Corona-Warn-App/src/device/java/de.rki.coronawarnapp/ui/main/MainFragment.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/ui/main/MainFragment.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.ui.main import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.widget.PopupMenu import androidx.fragment.app.Fragment @@ -16,12 +14,12 @@ import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -36,32 +34,19 @@ import kotlinx.coroutines.withContext * @see submissionViewModel * @see PopupMenu */ -class MainFragment : Fragment() { - - companion object { - private val TAG: String? = MainFragment::class.simpleName - } - +class MainFragment : Fragment(R.layout.fragment_main) { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() private val submissionViewModel: SubmissionViewModel by activityViewModels() - private var binding: FragmentMainBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentMainBinding.inflate(inflater) + private val binding: FragmentMainBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.submissionViewModel = submissionViewModel binding.lifecycleOwner = this - return binding.root - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() setContentDescription() diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/TestRiskLevelCalculationFragment.kt deleted file mode 100644 index 268a583555dcfa109ed6f2248f20ca53b19e726e..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/TestRiskLevelCalculationFragment.kt +++ /dev/null @@ -1,356 +0,0 @@ -package de.rki.coronawarnapp.test - -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.EditText -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewModelScope -import com.google.android.gms.nearby.Nearby -import com.google.android.gms.nearby.exposurenotification.ExposureInformation -import com.google.zxing.integration.android.IntentIntegrator -import com.google.zxing.integration.android.IntentResult -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import de.rki.coronawarnapp.risk.DefaultRiskLevelCalculation -import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange -import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService -import de.rki.coronawarnapp.sharing.ExposureSharingService -import de.rki.coronawarnapp.storage.AppDatabase -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction -import de.rki.coronawarnapp.transaction.RiskLevelTransaction -import de.rki.coronawarnapp.ui.viewLifecycle -import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel -import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel -import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel -import de.rki.coronawarnapp.util.KeyFileHelper -import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.util.security.SecurityHelper -import kotlinx.android.synthetic.deviceForTesters.fragment_test_risk_level_calculation.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.io.File -import java.util.UUID -import java.util.concurrent.TimeUnit -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -@Suppress("MagicNumber", "LongMethod") -class TestRiskLevelCalculationFragment : Fragment() { - companion object { - val TAG: String? = TestRiskLevelCalculationFragment::class.simpleName - } - - private val tracingViewModel: TracingViewModel by activityViewModels() - private val settingsViewModel: SettingsViewModel by activityViewModels() - private val submissionViewModel: SubmissionViewModel by activityViewModels() - private var binding: FragmentTestRiskLevelCalculationBinding by viewLifecycle() - - // reference to the client from the Google framework with the given application context - private val exposureNotificationClient by lazy { - Nearby.getExposureNotificationClient(CoronaWarnApplication.getAppContext()) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentTestRiskLevelCalculationBinding.inflate(inflater) - binding.tracingViewModel = tracingViewModel - binding.settingsViewModel = settingsViewModel - binding.submissionViewModel = submissionViewModel - binding.lifecycleOwner = this - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.buttonRetrieveDiagnosisKeys.setOnClickListener { - tracingViewModel.viewModelScope.launch { - retrieveDiagnosisKeys() - } - } - - binding.buttonProvideKeyViaQr.setOnClickListener { - scanLocalQRCodeAndProvide() - } - - binding.buttonCalculateRiskLevel.setOnClickListener { - tracingViewModel.viewModelScope.launch { - calculateRiskLevel() - } - } - - binding.buttonResetRiskLevel.setOnClickListener { - tracingViewModel.viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - // Preference reset - SecurityHelper.resetSharedPrefs() - // Database Reset - AppDatabase.reset(requireContext()) - // Export File Reset - AppInjector.component.keyCacheRepository.clear() - - LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) - LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) - LocalData.lastTimeDiagnosisKeysFromServerFetch(null) - LocalData.googleApiToken(null) - } catch (e: java.lang.Exception) { - e.report(ExceptionCategory.INTERNAL) - } - } - RiskLevelTransaction.start() - Toast.makeText( - requireContext(), "Reset done, please fetch diagnosis keys from server again", - Toast.LENGTH_SHORT - ).show() - } - } - - binding.buttonClearDiagnosisKeyCache.setOnClickListener { - lifecycleScope.launch { - AppInjector.component.keyCacheRepository.clear() - } - } - - startObserving() - } - - override fun onResume() { - super.onResume() - tracingViewModel.viewModelScope.launch { - calculateRiskLevel() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - - val result: IntentResult? = - IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - if (result != null) { - if (result.contents == null) { - Toast.makeText(requireContext(), "Cancelled", Toast.LENGTH_LONG).show() - } else { - ExposureSharingService.getOthersKeys(result.contents, onScannedKey) - } - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - private suspend fun retrieveDiagnosisKeys() { - try { - RetrieveDiagnosisKeysTransaction.start() - calculateRiskLevel() - } catch (e: TransactionException) { - e.report(ExceptionCategory.INTERNAL) - } - } - - private fun scanLocalQRCodeAndProvide() { - IntentIntegrator.forSupportFragment(this) - .setOrientationLocked(false) - .setBeepEnabled(false) - .initiateScan() - } - - private val onScannedKey = { key: AppleLegacyKeyExchange.Key? -> - Timber.i("keys scanned..") - provideDiagnosisKey(key) - } - - private fun provideDiagnosisKey(key: AppleLegacyKeyExchange.Key?) { - if (null == key) { - Toast.makeText(requireContext(), "No Key data found in QR code", Toast.LENGTH_SHORT) - .show() - } else { - val token = UUID.randomUUID().toString() - LocalData.googleApiToken(token) - - val appleKeyList = mutableListOf<AppleLegacyKeyExchange.Key>() - - val text = (transmission_number as EditText).text.toString() - var number = 5 - if (!text.isBlank()) { - number = Integer.valueOf(text) - } - - appleKeyList.add( - AppleLegacyKeyExchange.Key.newBuilder() - .setKeyData(key.keyData) - .setRollingPeriod(144) - .setRollingStartNumber(key.rollingStartNumber) - .setTransmissionRiskLevel(number) - .build() - ) - - val appleFiles = listOf( - AppleLegacyKeyExchange.File.newBuilder() - .addAllKeys(appleKeyList) - .build() - ) - - val dir = - File(File(requireContext().getExternalFilesDir(null), "key-export"), token) - dir.mkdirs() - - var googleFileList: List<File> - lifecycleScope.launch { - googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir) - - Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token") - try { - // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API - InternalExposureNotificationClient.asyncProvideDiagnosisKeys( - googleFileList, - ApplicationConfigurationService.asyncRetrieveExposureConfiguration(), - token - ) - Toast.makeText( - requireContext(), - "Provided ${appleKeyList.size} keys to Google API with token $token", - Toast.LENGTH_SHORT - ).show() - } catch (e: Exception) { - e.report(ExceptionCategory.EXPOSURENOTIFICATION) - } - } - } - } - - private suspend fun calculateRiskLevel() { - try { - RiskLevelTransaction.start() - } catch (e: TransactionException) { - e.report(ExceptionCategory.INTERNAL) - } - } - - private fun startObserving() { - tracingViewModel.viewModelScope.launch { - try { - val googleToken = LocalData.googleApiToken() ?: UUID.randomUUID().toString() - val exposureSummary = - InternalExposureNotificationClient.asyncGetExposureSummary(googleToken) - - val appConfig = - ApplicationConfigurationService.asyncRetrieveApplicationConfiguration() - - val riskLevelScore = DefaultRiskLevelCalculation().calculateRiskScore( - appConfig.attenuationDuration, - exposureSummary - ) - - val riskAsString = "Level: ${RiskLevelRepository.getLastCalculatedScore()}\n" + - "Last successful Level: " + - "${LocalData.lastSuccessfullyCalculatedRiskLevel()}\n" + - "Calculated Score: ${riskLevelScore}\n" + - "Last Time Server Fetch: ${LocalData.lastTimeDiagnosisKeysFromServerFetch()}\n" + - "Tracing Duration: " + - "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" + - "Tracing Duration in last 14 days: " + - "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days \n" + - "Last time risk level calculation ${LocalData.lastTimeRiskLevelCalculation()}" - - binding.labelRiskScore.text = riskAsString - - val lowClass = - appConfig.riskScoreClasses?.riskClassesList?.find { low -> low.label == "LOW" } - val highClass = - appConfig.riskScoreClasses?.riskClassesList?.find { high -> high.label == "HIGH" } - - val configAsString = - "Attenuation Weight Low: ${appConfig.attenuationDuration?.weights?.low}\n" + - "Attenuation Weight Mid: ${appConfig.attenuationDuration?.weights?.mid}\n" + - "Attenuation Weight High: ${appConfig.attenuationDuration?.weights?.high}\n\n" + - "Attenuation Offset: ${appConfig.attenuationDuration?.defaultBucketOffset}\n" + - "Attenuation Normalization: " + - "${appConfig.attenuationDuration?.riskScoreNormalizationDivisor}\n\n" + - "Risk Score Low Class: ${lowClass?.min ?: 0} - ${lowClass?.max ?: 0}\n" + - "Risk Score High Class: ${highClass?.min ?: 0} - ${highClass?.max ?: 0}" - - binding.labelBackendParameters.text = configAsString - - val summaryAsString = - "Days Since Last Exposure: ${exposureSummary.daysSinceLastExposure}\n" + - "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" + - "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" + - "Attenuation Durations: [${exposureSummary.attenuationDurationsInMinutes?.get( - 0 - )}," + - "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," + - "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" + - "Summation Risk Score: ${exposureSummary.summationRiskScore}" - - binding.labelExposureSummary.text = summaryAsString - - val maxRisk = exposureSummary.maximumRiskScore - val atWeights = appConfig.attenuationDuration?.weights - val attenuationDurationInMin = - exposureSummary.attenuationDurationsInMinutes - val attenuationConfig = appConfig.attenuationDuration - val formulaString = - "($maxRisk / ${attenuationConfig?.riskScoreNormalizationDivisor}) * " + - "(${attenuationDurationInMin?.get(0)} * ${atWeights?.low} " + - "+ ${attenuationDurationInMin?.get(1)} * ${atWeights?.mid} " + - "+ ${attenuationDurationInMin?.get(2)} * ${atWeights?.high} " + - "+ ${attenuationConfig?.defaultBucketOffset})" - - binding.labelFormula.text = formulaString - - binding.labelFullConfig.text = appConfig.toString() - - val token = LocalData.googleApiToken() - if (token != null) { - val exposureInformation = asyncGetExposureInformation(token) - - var infoString = "" - exposureInformation.forEach { - infoString += "Attenuation duration in min.: " + - "[${it.attenuationDurationsInMinutes?.get(0)}, " + - "${it.attenuationDurationsInMinutes?.get(1)}," + - "${it.attenuationDurationsInMinutes?.get(2)}]\n" + - "Attenuation value: ${it.attenuationValue}\n" + - "Duration in min.: ${it.durationMinutes}\n" + - "Risk Score: ${it.totalRiskScore}\n" + - "Transmission Risk Level: ${it.transmissionRiskLevel}\n" + - "Date Millis Since Epoch: ${it.dateMillisSinceEpoch}\n\n" - } - - binding.labelExposureInfo.text = infoString - } - } catch (e: Exception) { - e.report(ExceptionCategory.EXPOSURENOTIFICATION) - } - } - } - - suspend fun asyncGetExposureInformation(token: String): List<ExposureInformation> = - suspendCoroutine { cont -> - exposureNotificationClient.getExposureInformation(token) - .addOnSuccessListener { - cont.resume(it) - }.addOnFailureListener { - cont.resumeWithException(it) - } - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/TestForAPIFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt similarity index 94% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/TestForAPIFragment.kt rename to Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt index 15b56532f1ddd85ff49a0bf773ccead970ec028b..f7094337bcd777cb4f52ec1e89e2fface92cbe1c 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test +package de.rki.coronawarnapp.test.api.ui import android.content.Context import android.content.Intent @@ -52,10 +52,15 @@ import de.rki.coronawarnapp.storage.ExposureSummaryRepository import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository import de.rki.coronawarnapp.transaction.RiskLevelTransaction -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.KeyFileHelper import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import kotlinx.android.synthetic.deviceForTesters.fragment_test_for_a_p_i.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -65,9 +70,14 @@ import timber.log.Timber import java.io.File import java.lang.reflect.Type import java.util.UUID +import javax.inject.Inject @SuppressWarnings("TooManyFunctions", "MagicNumber", "LongMethod") -class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHelper.Callback { +class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), + InternalExposureNotificationPermissionHelper.Callback, AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: TestForApiFragmentViewModel by cwaViewModels { viewModelFactory } companion object { const val CONFIG_SCORE = 8 @@ -98,32 +108,15 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel private lateinit var qrPagerAdapter: RecyclerView.Adapter<QRPagerAdapter.QRViewHolder> // Data and View binding - private var binding: FragmentTestForAPIBinding by viewLifecycle() + private val binding: FragmentTestForAPIBinding by viewBindingLazy() private var lastSetCountries: List<String>? = null - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - - // get the binding reference by inflating it with the current layout - binding = FragmentTestForAPIBinding.inflate(inflater) - - // set the viewmmodel variable that will be used for data binding - binding.tracingViewModel = tracingViewModel - - // set the lifecycleowner for LiveData - binding.lifecycleOwner = this - - // Inflate the layout for this fragment - return binding.root - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.tracingViewModel = tracingViewModel + val v: Long = PackageInfoCompat.getLongVersionCode( activity?.packageManager!!.getPackageInfo( GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE, @@ -170,9 +163,11 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel val last3HoursSwitch = binding.testApiSwitchLastThreeHoursFromServer last3HoursSwitch.isChecked = LocalData.last3HoursMode() last3HoursSwitch.setOnClickListener { - val isLast3HoursModeEnabled = last3HoursSwitch.isChecked - showToast("Last 3 Hours Mode is activated: $isLast3HoursModeEnabled") - LocalData.last3HoursMode(isLast3HoursModeEnabled) + vm.setLast3HoursMode(last3HoursSwitch.isChecked) + } + + vm.last3HourToggleEvent.observe2(this) { + showToast("Last 3 Hours Mode is activated: $it") } val backgroundNotificationSwitch = binding.testApiSwitchBackgroundNotifications @@ -354,7 +349,6 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel key?.let { binding.textScannedKey.text = prettyKey(key) binding.textScannedKey.visibility = View.VISIBLE -// text_scanned_key.movementMethod = ScrollingMovementMethod.getInstance() } otherExposureKeyList.add(key!!) otherExposureKey = key @@ -512,7 +506,9 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel binding.labelMyKeys.text = myKeysLabelAndCount binding.textMyKeys.text = myExposureKeysJSON - myKeys?.maxBy { it.rollingStartIntervalNumber }?.rollingStartIntervalNumber?.toLong() + myKeys + ?.maxByOrNull { it.rollingStartIntervalNumber } + ?.rollingStartIntervalNumber?.toLong() ?.let { val ms = it * 60L * 10L * 1000L val dateString = DateTime(ms, DateTimeZone.UTC) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..38504b80936698889c42139f31388e5d4cf1f2e9 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentModule.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.test.api.ui + +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 TestForApiFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(TestForApiFragmentViewModel::class) + abstract fun testRiskLevelFragment(factory: TestForApiFragmentViewModel.Factory): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..10abbd1afbd9a95163f3c6a2fa098b0b967d54cf --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.test.api.ui + +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class TestForApiFragmentViewModel @AssistedInject constructor() : CWAViewModel() { + + val last3HourToggleEvent = SingleLiveEvent<Boolean>() + + fun setLast3HoursMode(isLast3HoursModeEnabled: Boolean) { + LocalData.last3HoursMode(isLast3HoursModeEnabled) + last3HourToggleEvent.postValue(isLast3HoursModeEnabled) + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<TestForApiFragmentViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..710555ae5067a96f872f02cdb7ff3caaad3477dc --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt @@ -0,0 +1,125 @@ +package de.rki.coronawarnapp.test.risklevel.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.navArgs +import com.google.zxing.integration.android.IntentIntegrator +import com.google.zxing.integration.android.IntentResult +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding +import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange +import de.rki.coronawarnapp.sharing.ExposureSharingService +import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel +import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel +import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +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.cwaViewModelsAssisted +import timber.log.Timber +import javax.inject.Inject + +@Suppress("MagicNumber", "LongMethod") +class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_level_calculation), + AutoInject { + private val navArgs by navArgs<TestRiskLevelCalculationFragmentArgs>() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: TestRiskLevelCalculationFragmentCWAViewModel by cwaViewModelsAssisted( + { viewModelFactory }, + { factory, handle -> + factory as TestRiskLevelCalculationFragmentCWAViewModel.Factory + factory.create(handle, navArgs.exampleArgument) + } + ) + + private val tracingViewModel: TracingViewModel by activityViewModels() + private val settingsViewModel: SettingsViewModel by activityViewModels() + private val submissionViewModel: SubmissionViewModel by activityViewModels() + + private val binding: FragmentTestRiskLevelCalculationBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.tracingViewModel = tracingViewModel + binding.settingsViewModel = settingsViewModel + binding.submissionViewModel = submissionViewModel + + binding.buttonRetrieveDiagnosisKeys.setOnClickListener { vm.retrieveDiagnosisKeys() } + binding.buttonProvideKeyViaQr.setOnClickListener { vm.scanLocalQRCodeAndProvide() } + binding.buttonCalculateRiskLevel.setOnClickListener { vm.calculateRiskLevel() } + binding.buttonClearDiagnosisKeyCache.setOnClickListener { vm.clearKeyCache() } + + binding.buttonResetRiskLevel.setOnClickListener { vm.resetRiskLevel() } + vm.riskLevelResetEvent.observe2(this) { + Toast.makeText( + requireContext(), "Reset done, please fetch diagnosis keys from server again", + Toast.LENGTH_SHORT + ).show() + } + + vm.riskScoreState.observe2(this) { state -> + binding.labelRiskScore.text = state.riskScoreMsg + binding.labelBackendParameters.text = state.backendParameters + binding.labelExposureSummary.text = state.exposureSummary + binding.labelFormula.text = state.formula + binding.labelFullConfig.text = state.fullConfig + binding.labelExposureInfo.text = state.exposureInfo + } + vm.startENFObserver() + + vm.apiKeysProvidedEvent.observe2(this) { event -> + Toast.makeText( + requireContext(), + "Provided ${event.keyCount} keys to Google API with token ${event.token}", + Toast.LENGTH_SHORT + ).show() + } + + vm.startLocalQRCodeScanEvent.observe2(this) { + IntentIntegrator.forSupportFragment(this) + .setOrientationLocked(false) + .setBeepEnabled(false) + .initiateScan() + } + } + + override fun onResume() { + super.onResume() + vm.calculateRiskLevel() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + val result: IntentResult = + IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + ?: return super.onActivityResult(requestCode, resultCode, data) + + if (result.contents == null) { + Toast.makeText(requireContext(), "Cancelled", Toast.LENGTH_LONG).show() + return + } + + ExposureSharingService.getOthersKeys(result.contents) { key: AppleLegacyKeyExchange.Key? -> + Timber.i("Keys scanned: %s", key) + if (key == null) { + Toast.makeText( + requireContext(), "No Key data found in QR code", Toast.LENGTH_SHORT + ).show() + return@getOthersKeys Unit + } + + val text = binding.transmissionNumber.text.toString() + val number = if (!text.isBlank()) Integer.valueOf(text) else 5 + vm.provideDiagnosisKey(number, key) + } + } + + companion object { + val TAG: String = TestRiskLevelCalculationFragment::class.simpleName!! + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..5628d6c968e6d6870818ee6cca169335f89d306f --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -0,0 +1,296 @@ +package de.rki.coronawarnapp.test.risklevel.ui + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.google.android.gms.nearby.exposurenotification.ExposureInformation +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.TransactionException +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.risk.DefaultRiskLevelCalculation +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.TimeVariables +import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange +import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService +import de.rki.coronawarnapp.storage.AppDatabase +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.transaction.RiskLevelTransaction +import de.rki.coronawarnapp.util.KeyFileHelper +import de.rki.coronawarnapp.util.security.SecurityHelper +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.util.UUID +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( + @Assisted private val handle: SavedStateHandle, + @Assisted private val exampleArg: String?, + private val context: Context, // App context + private val exposureNotificationClient: ExposureNotificationClient, + private val keyCacheRepository: KeyCacheRepository +) : CWAViewModel() { + + val startLocalQRCodeScanEvent = SingleLiveEvent<Unit>() + val riskLevelResetEvent = SingleLiveEvent<Unit>() + val apiKeysProvidedEvent = SingleLiveEvent<DiagnosisKeyProvidedEvent>() + val riskScoreState = MutableLiveData<RiskScoreState>(RiskScoreState()) + + init { + Timber.d("CWAViewModel: %s", this) + Timber.d("SavedStateHandle: %s", handle) + Timber.d("Example arg: %s", exampleArg) + } + + fun retrieveDiagnosisKeys() { + viewModelScope.launch { + try { + RetrieveDiagnosisKeysTransaction.start() + calculateRiskLevel() + } catch (e: TransactionException) { + e.report(ExceptionCategory.INTERNAL) + } + } + } + + fun calculateRiskLevel() { + viewModelScope.launch { + try { + RiskLevelTransaction.start() + } catch (e: TransactionException) { + e.report(ExceptionCategory.INTERNAL) + } + } + } + + fun resetRiskLevel() { + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + // Preference reset + SecurityHelper.resetSharedPrefs() + // Database Reset + AppDatabase.reset(context) + // Export File Reset + keyCacheRepository.clear() + + LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) + LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) + LocalData.lastTimeDiagnosisKeysFromServerFetch(null) + LocalData.googleApiToken(null) + } catch (e: Exception) { + e.report(ExceptionCategory.INTERNAL) + } + } + RiskLevelTransaction.start() + riskLevelResetEvent.postValue(Unit) + } + } + + data class RiskScoreState( + val riskScoreMsg: String = "", + val backendParameters: String = "", + val exposureSummary: String = "", + val formula: String = "", + val fullConfig: String = "", + val exposureInfo: String = "" + ) + + fun startENFObserver() { + viewModelScope.launch { + try { + var workState = riskScoreState.value!! + + val googleToken = LocalData.googleApiToken() ?: UUID.randomUUID().toString() + val exposureSummary = + InternalExposureNotificationClient.asyncGetExposureSummary(googleToken) + + val appConfig = + ApplicationConfigurationService.asyncRetrieveApplicationConfiguration() + + val riskLevelScore = DefaultRiskLevelCalculation().calculateRiskScore( + appConfig.attenuationDuration, + exposureSummary + ) + + val riskAsString = "Level: ${RiskLevelRepository.getLastCalculatedScore()}\n" + + "Last successful Level: " + + "${LocalData.lastSuccessfullyCalculatedRiskLevel()}\n" + + "Calculated Score: ${riskLevelScore}\n" + + "Last Time Server Fetch: ${LocalData.lastTimeDiagnosisKeysFromServerFetch()}\n" + + "Tracing Duration: " + + "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" + + "Tracing Duration in last 14 days: " + + "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days \n" + + "Last time risk level calculation ${LocalData.lastTimeRiskLevelCalculation()}" + + workState = workState.copy(riskScoreMsg = riskAsString) + + val lowClass = + appConfig.riskScoreClasses?.riskClassesList?.find { low -> low.label == "LOW" } + val highClass = + appConfig.riskScoreClasses?.riskClassesList?.find { high -> high.label == "HIGH" } + + val configAsString = + "Attenuation Weight Low: ${appConfig.attenuationDuration?.weights?.low}\n" + + "Attenuation Weight Mid: ${appConfig.attenuationDuration?.weights?.mid}\n" + + "Attenuation Weight High: ${appConfig.attenuationDuration?.weights?.high}\n\n" + + "Attenuation Offset: ${appConfig.attenuationDuration?.defaultBucketOffset}\n" + + "Attenuation Normalization: " + + "${appConfig.attenuationDuration?.riskScoreNormalizationDivisor}\n\n" + + "Risk Score Low Class: ${lowClass?.min ?: 0} - ${lowClass?.max ?: 0}\n" + + "Risk Score High Class: ${highClass?.min ?: 0} - ${highClass?.max ?: 0}" + + workState = workState.copy(backendParameters = configAsString) + + val summaryAsString = + "Days Since Last Exposure: ${exposureSummary.daysSinceLastExposure}\n" + + "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" + + "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" + + "Attenuation Durations: [${ + exposureSummary.attenuationDurationsInMinutes?.get( + 0 + ) + }," + + "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," + + "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" + + "Summation Risk Score: ${exposureSummary.summationRiskScore}" + + workState = workState.copy(exposureSummary = summaryAsString) + + val maxRisk = exposureSummary.maximumRiskScore + val atWeights = appConfig.attenuationDuration?.weights + val attenuationDurationInMin = + exposureSummary.attenuationDurationsInMinutes + val attenuationConfig = appConfig.attenuationDuration + val formulaString = + "($maxRisk / ${attenuationConfig?.riskScoreNormalizationDivisor}) * " + + "(${attenuationDurationInMin?.get(0)} * ${atWeights?.low} " + + "+ ${attenuationDurationInMin?.get(1)} * ${atWeights?.mid} " + + "+ ${attenuationDurationInMin?.get(2)} * ${atWeights?.high} " + + "+ ${attenuationConfig?.defaultBucketOffset})" + + workState = + workState.copy(formula = formulaString, fullConfig = appConfig.toString()) + + val token = LocalData.googleApiToken() + if (token != null) { + val exposureInformation = asyncGetExposureInformation(token) + + var infoString = "" + exposureInformation.forEach { + infoString += "Attenuation duration in min.: " + + "[${it.attenuationDurationsInMinutes?.get(0)}, " + + "${it.attenuationDurationsInMinutes?.get(1)}," + + "${it.attenuationDurationsInMinutes?.get(2)}]\n" + + "Attenuation value: ${it.attenuationValue}\n" + + "Duration in min.: ${it.durationMinutes}\n" + + "Risk Score: ${it.totalRiskScore}\n" + + "Transmission Risk Level: ${it.transmissionRiskLevel}\n" + + "Date Millis Since Epoch: ${it.dateMillisSinceEpoch}\n\n" + } + + workState = workState.copy(exposureInfo = infoString) + } + + riskScoreState.postValue(workState) + } catch (e: Exception) { + e.report(ExceptionCategory.EXPOSURENOTIFICATION) + } + } + } + + private suspend fun asyncGetExposureInformation(token: String): List<ExposureInformation> = + suspendCoroutine { cont -> + exposureNotificationClient.getExposureInformation(token) + .addOnSuccessListener { + cont.resume(it) + }.addOnFailureListener { + cont.resumeWithException(it) + } + } + + data class DiagnosisKeyProvidedEvent( + val keyCount: Int, + val token: String + ) + + fun provideDiagnosisKey(transmissionNumber: Int, key: AppleLegacyKeyExchange.Key) { + val token = UUID.randomUUID().toString() + LocalData.googleApiToken(token) + + val appleKeyList = mutableListOf<AppleLegacyKeyExchange.Key>() + + AppleLegacyKeyExchange.Key.newBuilder() + .setKeyData(key.keyData) + .setRollingPeriod(144) + .setRollingStartNumber(key.rollingStartNumber) + .setTransmissionRiskLevel(transmissionNumber) + .build() + .also { appleKeyList.add(it) } + + val appleFiles = listOf( + AppleLegacyKeyExchange.File.newBuilder() + .addAllKeys(appleKeyList) + .build() + ) + + val dir = File(File(context.getExternalFilesDir(null), "key-export"), token) + dir.mkdirs() + + var googleFileList: List<File> + viewModelScope.launch { + googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir) + + Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token") + try { + // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API + InternalExposureNotificationClient.asyncProvideDiagnosisKeys( + googleFileList, + ApplicationConfigurationService.asyncRetrieveExposureConfiguration(), + token + ) + apiKeysProvidedEvent.postValue( + DiagnosisKeyProvidedEvent( + keyCount = appleFiles.size, + token = token + ) + ) + } catch (e: Exception) { + e.report(ExceptionCategory.EXPOSURENOTIFICATION) + } + } + } + + fun scanLocalQRCodeAndProvide() { + startLocalQRCodeScanEvent.postValue(Unit) + } + + fun clearKeyCache() { + viewModelScope.launch { keyCacheRepository.clear() } + } + + @AssistedInject.Factory + interface Factory : CWAViewModelFactory<TestRiskLevelCalculationFragmentCWAViewModel> { + fun create( + handle: SavedStateHandle, + exampleArg: String? + ): TestRiskLevelCalculationFragmentCWAViewModel + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..1824e96499f7de067d31fa2630bf874b13d30cf5 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentModule.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.test.risklevel.ui + +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 TestRiskLevelCalculationFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(TestRiskLevelCalculationFragmentCWAViewModel::class) + abstract fun testRiskLevelFragment(factory: TestRiskLevelCalculationFragmentCWAViewModel.Factory): CWAViewModelFactory<out CWAViewModel> +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..e4371986903d63f93649895ecb25372344ad7e2c --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.main + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.test.api.ui.TestForAPIFragment +import de.rki.coronawarnapp.test.api.ui.TestForApiFragmentModule +import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment +import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragmentModule + +@Module +abstract class MainActivityTestModule { + + @ContributesAndroidInjector(modules = [TestRiskLevelCalculationFragmentModule::class]) + abstract fun testRiskLevelCalculationFragment(): TestRiskLevelCalculationFragment + + @ContributesAndroidInjector(modules = [TestForApiFragmentModule::class]) + abstract fun testRiskLevelApiFragment(): TestForAPIFragment +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainFragment.kt index add678b4840fa4a359551dc0edb97f8dea0ab5c0..1ae185f1a87a56320eb6d204153c41c9c7feca0b 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainFragment.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.ui.main import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.widget.PopupMenu import androidx.fragment.app.Fragment @@ -16,12 +14,12 @@ import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -36,32 +34,19 @@ import kotlinx.coroutines.withContext * @see submissionViewModel * @see PopupMenu */ -class MainFragment : Fragment() { - - companion object { - private val TAG: String? = MainFragment::class.simpleName - } +class MainFragment : Fragment(R.layout.fragment_main) { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() private val submissionViewModel: SubmissionViewModel by activityViewModels() - private var binding: FragmentMainBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentMainBinding.inflate(inflater) + private val binding: FragmentMainBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.submissionViewModel = submissionViewModel - binding.lifecycleOwner = this - return binding.root - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() showOneTimeTracingExplanationDialog() @@ -171,7 +156,9 @@ class MainFragment : Fragment() { } R.id.menu_test_risk_level -> { findNavController().doNavigate( - MainFragmentDirections.actionMainFragmentToTestRiskLevelCalculation() + MainFragmentDirections.actionMainFragmentToTestRiskLevelCalculation( + exampleArgument = null + ) ) true } diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml index 29c72aa41190ca9bb3b03a6318f1e155d74c97f9..43fb6ab2caa3ce823f5a146236074240695ab3f8 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml @@ -174,7 +174,7 @@ <!-- Submission --> <fragment android:id="@+id/testForAPIFragment" - android:name="de.rki.coronawarnapp.test.TestForAPIFragment" + android:name="de.rki.coronawarnapp.test.api.ui.TestForAPIFragment" android:label="@layout/fragment_test_for_a_p_i" tools:layout="@layout/fragment_test_for_a_p_i" /> @@ -315,7 +315,12 @@ </fragment> <fragment android:id="@+id/testRiskLevelCalculation" - android:name="de.rki.coronawarnapp.test.TestRiskLevelCalculationFragment" + android:name="de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment" android:label="fragment_test_risk_level_calculation" - tools:layout="@layout/fragment_test_risk_level_calculation" /> + tools:layout="@layout/fragment_test_risk_level_calculation"> + <argument + android:name="exampleArgument" + app:argType="string" + app:nullable="true" /> + </fragment> </navigation> diff --git a/Corona-Warn-App/src/main/assets/technical_de.html b/Corona-Warn-App/src/main/assets/technical_de.html index 8fef2897c5e5f5d4e4fd0e5d0f06ab0db5d5fd31..cb8322cc84be6d36cdaf9fe0731b9a6bab8317f6 100644 --- a/Corona-Warn-App/src/main/assets/technical_de.html +++ b/Corona-Warn-App/src/main/assets/technical_de.html @@ -62,6 +62,14 @@ Licensor: Google Website: https://github.com/google/protobuf-gradle-plugin License: BSD 3-Clause</p> +<p>Component: Dagger + Licensor: Google + Website: https://github.com/google/dagger + License: Apache 2.0</p> +<p>Component: AssistedInject + Licensor: Square + Website: https://github.com/square/AssistedInject + License: Apache 2.0</p> <p>Component: Conscrypt Licensor: Conscrypt Website: https://github.com/google/conscrypt diff --git a/Corona-Warn-App/src/main/assets/technical_en.html b/Corona-Warn-App/src/main/assets/technical_en.html index 8fef2897c5e5f5d4e4fd0e5d0f06ab0db5d5fd31..cb8322cc84be6d36cdaf9fe0731b9a6bab8317f6 100644 --- a/Corona-Warn-App/src/main/assets/technical_en.html +++ b/Corona-Warn-App/src/main/assets/technical_en.html @@ -62,6 +62,14 @@ Licensor: Google Website: https://github.com/google/protobuf-gradle-plugin License: BSD 3-Clause</p> +<p>Component: Dagger + Licensor: Google + Website: https://github.com/google/dagger + License: Apache 2.0</p> +<p>Component: AssistedInject + Licensor: Square + Website: https://github.com/square/AssistedInject + License: Apache 2.0</p> <p>Component: Conscrypt Licensor: Conscrypt Website: https://github.com/google/conscrypt diff --git a/Corona-Warn-App/src/main/assets/technical_tr.html b/Corona-Warn-App/src/main/assets/technical_tr.html index 8fef2897c5e5f5d4e4fd0e5d0f06ab0db5d5fd31..cb8322cc84be6d36cdaf9fe0731b9a6bab8317f6 100644 --- a/Corona-Warn-App/src/main/assets/technical_tr.html +++ b/Corona-Warn-App/src/main/assets/technical_tr.html @@ -62,6 +62,14 @@ Licensor: Google Website: https://github.com/google/protobuf-gradle-plugin License: BSD 3-Clause</p> +<p>Component: Dagger + Licensor: Google + Website: https://github.com/google/dagger + License: Apache 2.0</p> +<p>Component: AssistedInject + Licensor: Square + Website: https://github.com/square/AssistedInject + License: Apache 2.0</p> <p>Component: Conscrypt Licensor: Conscrypt Website: https://github.com/google/conscrypt diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index 9369e2914894cdf8b15bc3ac7f84691c90165bbe..cd26d162082c879c4b44e3a4beba10c9c18403e8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -65,12 +65,12 @@ class CoronaWarnApplication : Application(), LifecycleObserver, @Inject lateinit var androidInjector: DispatchingAndroidInjector<Any> + override fun androidInjector(): AndroidInjector<Any> = androidInjector override fun onCreate() { - AppInjector.init(this) - - super.onCreate() instance = this + super.onCreate() + AppInjector.init(this) if (CWADebug.isDebugBuildOrMode) System.setProperty("kotlinx.coroutines.debug", "on") @@ -197,6 +197,4 @@ class CoronaWarnApplication : Application(), LifecycleObserver, LocalBroadcastManager.getInstance(this) .registerReceiver(errorReceiver, IntentFilter(ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)) } - - override fun androidInjector(): AndroidInjector<Any> = androidInjector } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/NearbyModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/NearbyModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..e35491eafaf8d414ee359c84ba80a7567db619bb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/NearbyModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.nearby + +import android.content.Context +import com.google.android.gms.nearby.Nearby +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import dagger.Module +import dagger.Provides +import dagger.Reusable + +@Module +class NearbyModule { + + @Reusable + @Provides + fun provideENF(context: Context): ExposureNotificationClient { + return Nearby.getExposureNotificationClient(context) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt index ac56b4c5df61a79b2d19a485ec7e876674aceb14..96c90c0f6beb744030269b9f6268694af52c425c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt @@ -120,7 +120,6 @@ object TimerHelper { * @see getManualKeyRetrievalTimeLeft * @see SettingsRepository.updateManualKeyRetrievalEnabled * @see SettingsRepository.updateManualKeyRetrievalTime - * @see de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHMS */ private fun onManualKeyRetrievalTimerTick() { val timeDifference = getManualKeyRetrievalTimeLeft() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt index aaf402da012fd5a83cd513490e90745b6803ddf5..011e464a4139ce61ae3d27277f58feee268fd9ef 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt @@ -4,12 +4,13 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.main.MainActivityModule +import de.rki.coronawarnapp.ui.main.MainActivityTestModule import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivityModule @Module abstract class ActivityBinder { - @ContributesAndroidInjector(modules = [MainActivityModule::class]) + @ContributesAndroidInjector(modules = [MainActivityModule::class, MainActivityTestModule::class]) abstract fun mainActivity(): MainActivity @ContributesAndroidInjector diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt index c5e7e7d00060b30ac1aeb7d32b24a97794e89d39..1f55312ecea5c8dd259a6445f235aca801693e86 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt @@ -3,11 +3,11 @@ package de.rki.coronawarnapp.ui import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import dagger.android.AndroidInjection import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity import de.rki.coronawarnapp.update.UpdateChecker +import de.rki.coronawarnapp.util.di.AppInjector import kotlinx.coroutines.launch class LauncherActivity : AppCompatActivity() { @@ -18,7 +18,7 @@ class LauncherActivity : AppCompatActivity() { private lateinit var updateChecker: UpdateChecker override fun onCreate(savedInstanceState: Bundle?) { - AndroidInjection.inject(this) + AppInjector.setup(this) super.onCreate(savedInstanceState) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt index ed36ea61f2a800d739395fcb5c751463386f02a2..ed1f696f784ab156db60f8c31b7db67cd5917bf2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt @@ -1,15 +1,7 @@ package de.rki.coronawarnapp.ui -import android.os.Handler -import android.os.Looper -import androidx.fragment.app.Fragment -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer import androidx.navigation.NavController import androidx.navigation.NavDirections -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty /** * Extends NavController to prevent navigation error when the user clicks on two buttons at almost @@ -21,47 +13,3 @@ fun NavController.doNavigate(direction: NavDirections) { currentDestination?.getAction(direction.actionId) ?.let { navigate(direction) } } - -/** - * An extension to bind and unbind a value based on the view lifecycle of a Fragment. - * The binding will be unbound in onDestroyView. - * - * @throws IllegalStateException If the getter is invoked before the binding is set, - * or after onDestroyView an exception is thrown. - */ -fun <T> Fragment.viewLifecycle(): ReadWriteProperty<Fragment, T> { - return object : ReadWriteProperty<Fragment, T>, DefaultLifecycleObserver { - - private var binding: T? = null - - init { - this@viewLifecycle - .viewLifecycleOwnerLiveData - .observe(this@viewLifecycle, Observer { owner: LifecycleOwner? -> - owner?.lifecycle?.addObserver(this) - }) - } - - override fun onDestroy(owner: LifecycleOwner) { - val handler = Handler(Looper.getMainLooper()) - handler.post { - binding = null - } - } - - override fun getValue( - thisRef: Fragment, - property: KProperty<*> - ): T { - return this.binding ?: error("Called before onCreateView or after onDestroyView.") - } - - override fun setValue( - thisRef: Fragment, - property: KProperty<*>, - value: T - ) { - this.binding = value - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt index 3b98d61d742bc64da47e392a3b429f306d01833c..e0e7143283072ad7e0f69c958fd67cbe954f78e1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt @@ -3,34 +3,20 @@ package de.rki.coronawarnapp.ui.information import android.content.Intent import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationAboutBinding import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which only displays static content. */ -class InformationAboutFragment : Fragment() { - companion object { - private val TAG: String? = InformationAboutFragment::class.simpleName - } - - private var binding: FragmentInformationAboutBinding by viewLifecycle() +class InformationAboutFragment : Fragment(R.layout.fragment_information_about) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationAboutBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentInformationAboutBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt index 5d42b024caaf792d54e0e7008c1f6295eda6db78..61f6ff9699e927de917d74d06715ba2ae978f1b0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt @@ -1,35 +1,21 @@ package de.rki.coronawarnapp.ui.information import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationContactBinding import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which only displays static content. */ -class InformationContactFragment : Fragment() { - companion object { - private val TAG: String? = InformationContactFragment::class.simpleName - } - - private var binding: FragmentInformationContactBinding by viewLifecycle() +class InformationContactFragment : Fragment(R.layout.fragment_information_contact) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationContactBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentInformationContactBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt index 418455b8bffdce3ed6fd8db125d7d022f5c9a866..e6b1683ce430f4073e569959a96785f79ae6eec2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.ui.information import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.fragment.app.Fragment @@ -12,27 +10,15 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationBinding import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which links to static and web content. */ -class InformationFragment : Fragment() { - companion object { - private val TAG: String? = InformationFragment::class.simpleName - } - - private var binding: FragmentInformationBinding by viewLifecycle() +class InformationFragment : Fragment(R.layout.fragment_information) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentInformationBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt index 62a42f82fdbb112b6b31e517d25f0700c106dc91..560b64eb7029a5b307952a82fcac44b6d9fce725 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt @@ -2,35 +2,21 @@ package de.rki.coronawarnapp.ui.information import android.os.Bundle import android.text.method.LinkMovementMethod -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationLegalBinding import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.convertToHyperlink -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which only displays static content. */ -class InformationLegalFragment : Fragment() { - companion object { - private val TAG: String? = InformationLegalFragment::class.simpleName - } - - private var binding: FragmentInformationLegalBinding by viewLifecycle() +class InformationLegalFragment : Fragment(R.layout.fragment_information_legal) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationLegalBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentInformationLegalBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt index 2aede5973617d47d3e8c77d9e15537ba52efb2c0..41c3cde335afb3b4368409540c49c09ba11a3233 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt @@ -1,33 +1,19 @@ package de.rki.coronawarnapp.ui.information import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationPrivacyBinding import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which only displays static content. */ -class InformationPrivacyFragment : Fragment() { - companion object { - private val TAG: String? = InformationPrivacyFragment::class.simpleName - } - - private var binding: FragmentInformationPrivacyBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationPrivacyBinding.inflate(inflater) - return binding.root - } +class InformationPrivacyFragment : Fragment(R.layout.fragment_information_privacy) { + private val binding: FragmentInformationPrivacyBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt index 193fde9c2640c0ff26792d77830ae978b6deaec2..0a98ff88bfbcd7352f9b0d3260b3116e8f069b5d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt @@ -1,33 +1,23 @@ package de.rki.coronawarnapp.ui.information import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationTechnicalBinding import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which only displays static content. */ -class InformationTechnicalFragment : Fragment() { +class InformationTechnicalFragment : Fragment(R.layout.fragment_information_technical) { companion object { private val TAG: String? = InformationTechnicalFragment::class.simpleName } - private var binding: FragmentInformationTechnicalBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationTechnicalBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentInformationTechnicalBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt index 592c36df7c4a5949313f1b06580507292df0c3e0..5c0046ebea1ee3ed37a93482be699babbc0955c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt @@ -1,33 +1,20 @@ package de.rki.coronawarnapp.ui.information import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationTermsBinding import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Basic Fragment which only displays static content. */ -class InformationTermsFragment : Fragment() { - companion object { - private val TAG: String? = InformationTermsFragment::class.simpleName - } - - private var binding: FragmentInformationTermsBinding by viewLifecycle() +class InformationTermsFragment : Fragment(R.layout.fragment_information_terms) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentInformationTermsBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentInformationTermsBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index c4bbd54c91d8ae652a0d448a631a385145261591..63af50f06e1e574c7a09740c299981e97248b7ea 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -10,7 +10,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.lifecycleScope -import dagger.android.AndroidInjection +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector import de.rki.coronawarnapp.R import de.rki.coronawarnapp.http.playbook.BackgroundNoise import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient @@ -21,6 +23,7 @@ import de.rki.coronawarnapp.util.BackgroundPrioritization import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.device.PowerManagement +import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.launch import timber.log.Timber @@ -34,7 +37,7 @@ import javax.inject.Inject * @see ConnectivityHelper * @see BackgroundWorkScheduler */ -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), HasAndroidInjector { companion object { private val TAG: String? = MainActivity::class.simpleName @@ -43,6 +46,9 @@ class MainActivity : AppCompatActivity() { } } + @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> + override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector + private val FragmentManager.currentNavigationFragment: Fragment? get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first() @@ -89,14 +95,15 @@ class MainActivity : AppCompatActivity() { } override fun onLocationUnavailable() { - val canIgnoreLocationEnabled = InternalExposureNotificationClient.deviceSupportsLocationlessScanning() + val canIgnoreLocationEnabled = + InternalExposureNotificationClient.deviceSupportsLocationlessScanning() settingsViewModel.updateLocationEnabled(canIgnoreLocationEnabled) Timber.d("Location unavailable but can be ignored? $canIgnoreLocationEnabled") } } override fun onCreate(savedInstanceState: Bundle?) { - AndroidInjection.inject(this) + AppInjector.setup(this) super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) settingsViewModel = ViewModelProviders.of(this).get(SettingsViewModel::class.java) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt index aa437a80b60dcf6fa79874ef212652d6bb88a62d..c99a5dba307a8486871d86d3fd8929817ed8fb26 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt @@ -1,13 +1,12 @@ package de.rki.coronawarnapp.ui.main import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentMainOverviewBinding -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * The fragment displays static informative content to the user @@ -16,22 +15,9 @@ import de.rki.coronawarnapp.ui.viewLifecycle * */ -class MainOverviewFragment : Fragment() { +class MainOverviewFragment : Fragment(R.layout.fragment_main_overview) { - companion object { - private val TAG: String? = MainOverviewFragment::class.simpleName - } - - private var binding: FragmentMainOverviewBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentMainOverviewBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentMainOverviewBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt index 5dd447eaf6cf91b64866836419e60e930537efaf..a82e0c320d3edbd0dd6bc5dae69918206681f5c4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt @@ -1,45 +1,29 @@ package de.rki.coronawarnapp.ui.main import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentMainShareBinding -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This fragment informs the user about what he is going to share and how he is going to help everybody with this :) * * @see TracingViewModel */ -class MainShareFragment : Fragment() { - - companion object { - private val TAG: String? = MainShareFragment::class.simpleName - } +class MainShareFragment : Fragment(R.layout.fragment_main_share) { private val tracingViewModel: TracingViewModel by activityViewModels() - private var binding: FragmentMainShareBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentMainShareBinding.inflate(inflater) - binding.tracingViewModel = tracingViewModel - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentMainShareBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.tracingViewModel = tracingViewModel setButtonOnClickListener() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt index 81d9648ee0bfa308e26ba266bfc02846a4680be4..0e17e71226144c4da0f24ee5e76edb633ea3384a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt @@ -7,10 +7,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleObserver -import dagger.android.AndroidInjection import de.rki.coronawarnapp.R import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity +import de.rki.coronawarnapp.util.di.AppInjector /** * This activity holds all the onboarding fragments and isn't used after a successful onboarding flow. @@ -31,7 +31,7 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver { get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first() override fun onCreate(savedInstanceState: Bundle?) { - AndroidInjection.inject(this) + AppInjector.setup(this) super.onCreate(savedInstanceState) setContentView(R.layout.activity_onboarding) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingFragment.kt index b1daa10c00314fb9dae088f67cdecc786cc43a9c..df4b0d3f6735f76ff3e8f1f778ef88774802449a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingFragment.kt @@ -3,35 +3,21 @@ package de.rki.coronawarnapp.ui.onboarding import android.content.Intent import android.net.Uri import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentOnboardingBinding import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * Onboarding starting point. */ -class OnboardingFragment : Fragment() { - companion object { - private val TAG: String? = OnboardingFragment::class.simpleName - } - - private var binding: FragmentOnboardingBinding by viewLifecycle() +class OnboardingFragment : Fragment(R.layout.fragment_onboarding) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnboardingBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentOnboardingBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -46,10 +32,12 @@ class OnboardingFragment : Fragment() { private fun setLinks() { binding.onboardingInclude.onboardingEasyLanguage .setOnClickListener { - val browserIntent = Intent(Intent.ACTION_VIEW, - Uri.parse(getString(R.string.onboarding_tracing_easy_language_explanation_url))) - startActivity(browserIntent) - } + val browserIntent = Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.onboarding_tracing_easy_language_explanation_url)) + ) + startActivity(browserIntent) + } } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingNotificationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingNotificationsFragment.kt index 5813171099375402630300b5cf0dcccaf25069c7..7ebb2b1d16e1886134c81ebae426795184b9e380 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingNotificationsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingNotificationsFragment.kt @@ -1,15 +1,14 @@ package de.rki.coronawarnapp.ui.onboarding import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.appcompat.app.AlertDialog import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentOnboardingNotificationsBinding -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This fragment ask the user if he wants to get notifications and finishes the onboarding afterwards. @@ -17,21 +16,9 @@ import de.rki.coronawarnapp.ui.viewLifecycle * @see NotificationManagerCompat * @see AlertDialog */ -class OnboardingNotificationsFragment : Fragment() { - companion object { - private val TAG: String? = OnboardingNotificationsFragment::class.simpleName - } - - private var binding: FragmentOnboardingNotificationsBinding by viewLifecycle() +class OnboardingNotificationsFragment : Fragment(R.layout.fragment_onboarding_notifications) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnboardingNotificationsBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentOnboardingNotificationsBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingPrivacyFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingPrivacyFragment.kt index f81ec47d1e0a1524596815713ce7e24158371454..010b629c714a490b2f1e40d4f31123a46726c6c8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingPrivacyFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingPrivacyFragment.kt @@ -1,34 +1,21 @@ package de.rki.coronawarnapp.ui.onboarding import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentOnboardingPrivacyBinding import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This fragment informs the user regarding privacy. */ -class OnboardingPrivacyFragment : Fragment() { - companion object { - private val TAG: String? = OnboardingPrivacyFragment::class.simpleName - } - - private var binding: FragmentOnboardingPrivacyBinding by viewLifecycle() +class OnboardingPrivacyFragment : Fragment(R.layout.fragment_onboarding_privacy) { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnboardingPrivacyBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentOnboardingPrivacyBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTestFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTestFragment.kt index aacc99a5e1d01e2bf193804ceccda1a63408c86a..a66ac14e56298884c776a73c3e6395f7a0dab7c0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTestFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTestFragment.kt @@ -1,34 +1,20 @@ package de.rki.coronawarnapp.ui.onboarding import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentOnboardingTestBinding import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This fragment informs the user about test results. */ -class OnboardingTestFragment : Fragment() { - companion object { - private val TAG: String? = OnboardingTestFragment::class.simpleName - } - - private var binding: FragmentOnboardingTestBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnboardingTestBinding.inflate(inflater) - return binding.root - } +class OnboardingTestFragment : Fragment(R.layout.fragment_onboarding_test) { + private val binding: FragmentOnboardingTestBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt index 1d8e08fdffc83e8b9261f289162ba632a0d57480..de87c54f4b510af006247f8b04779729b210f72b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt @@ -2,9 +2,7 @@ package de.rki.coronawarnapp.ui.onboarding import android.content.Intent import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment @@ -18,8 +16,8 @@ import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy import kotlinx.coroutines.launch /** @@ -28,7 +26,7 @@ import kotlinx.coroutines.launch * @see InternalExposureNotificationPermissionHelper * @see AlertDialog */ -class OnboardingTracingFragment : Fragment(), +class OnboardingTracingFragment : Fragment(R.layout.fragment_onboarding_tracing), InternalExposureNotificationPermissionHelper.Callback { companion object { @@ -36,7 +34,7 @@ class OnboardingTracingFragment : Fragment(), } private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper - private var binding: FragmentOnboardingTracingBinding by viewLifecycle() + private val binding: FragmentOnboardingTracingBinding by viewBindingLazy() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { internalExposureNotificationPermissionHelper.onResolutionComplete( @@ -51,15 +49,6 @@ class OnboardingTracingFragment : Fragment(), InternalExposureNotificationPermissionHelper(this, this) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentOnboardingTracingBinding.inflate(inflater) - return binding.root - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() @@ -123,7 +112,7 @@ class OnboardingTracingFragment : Fragment(), } catch (exception: Exception) { exception.report( ExceptionCategory.EXPOSURENOTIFICATION, - OnboardingTracingFragment.TAG, + TAG, null ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt index db5bba738605699031475f19c236acc7ac115c1f..4a7512b80e8021f39a33b706e600b2140dfcd4e0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt @@ -1,20 +1,19 @@ package de.rki.coronawarnapp.ui.riskdetails import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentRiskDetailsBinding import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This is the detail view of the risk card if additional information for the user. @@ -22,30 +21,16 @@ import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel * @see TracingViewModel * @see SettingsViewModel */ -class RiskDetailsFragment : Fragment() { - - companion object { - private val TAG: String? = RiskDetailsFragment::class.simpleName - } +class RiskDetailsFragment : Fragment(R.layout.fragment_risk_details) { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() - private var binding: FragmentRiskDetailsBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentRiskDetailsBinding.inflate(inflater) - binding.tracingViewModel = tracingViewModel - binding.settingsViewModel = settingsViewModel - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentRiskDetailsBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.tracingViewModel = tracingViewModel + binding.settingsViewModel = settingsViewModel setButtonOnClickListeners() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsBackgroundPriorityFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsBackgroundPriorityFragment.kt index 7385447600b0312a69f06c32da52abd4f19491c9..57db3b076f914cd0d6c72fe88b423a9e67f1d5ae 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsBackgroundPriorityFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsBackgroundPriorityFragment.kt @@ -1,18 +1,17 @@ package de.rki.coronawarnapp.ui.settings import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSettingsBackgroundPriorityBinding import de.rki.coronawarnapp.ui.base.startActivitySafely import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This is the setting background priority page. Here the user sees the background priority setting status. @@ -21,27 +20,15 @@ import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel * @see TracingViewModel * @see SettingsViewModel */ -class SettingsBackgroundPriorityFragment : Fragment() { - companion object { - private val TAG: String? = SettingsBackgroundPriorityFragment::class.simpleName - } +class SettingsBackgroundPriorityFragment : + Fragment(R.layout.fragment_settings_background_priority) { private val settingsViewModel: SettingsViewModel by activityViewModels() - private var binding: FragmentSettingsBackgroundPriorityBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingsBackgroundPriorityBinding.inflate(inflater) - binding.settingsViewModel = settingsViewModel - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentSettingsBackgroundPriorityBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.settingsViewModel = settingsViewModel setButtonOnClickListener() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt index 1f6338e182d5c07b3c2c7b1012bd102aa2e47793..ab2dd1d05f46a23bf02313e24ad9cc9a59f47011 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt @@ -1,19 +1,18 @@ package de.rki.coronawarnapp.ui.settings import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSettingsBinding import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This is the setting overview page. @@ -21,30 +20,16 @@ import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel * @see TracingViewModel * @see SettingsViewModel */ -class SettingsFragment : Fragment() { - - companion object { - private val TAG: String? = SettingsFragment::class.simpleName - } +class SettingsFragment : Fragment(R.layout.fragment_settings) { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() - private var binding: FragmentSettingsBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingsBinding.inflate(inflater) - binding.tracingViewModel = tracingViewModel - binding.settingsViewModel = settingsViewModel - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentSettingsBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.tracingViewModel = tracingViewModel + binding.settingsViewModel = settingsViewModel setButtonOnClickListener() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt index 7cbbccbfe2ef4a63c71d85f85c3f1d6cf9c1888d..ce4ac665155a1d61dafea75d6212d1781b64a929 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt @@ -1,19 +1,18 @@ package de.rki.coronawarnapp.ui.settings import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSettingsNotificationsBinding import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.ExternalActionHelper import de.rki.coronawarnapp.util.IGNORE_CHANGE_TAG +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * This is the setting notification page. Here the user sees his os notifications settings status. @@ -23,27 +22,14 @@ import de.rki.coronawarnapp.util.IGNORE_CHANGE_TAG * @see TracingViewModel * @see SettingsViewModel */ -class SettingsNotificationFragment : Fragment() { - companion object { - private val TAG: String? = SettingsNotificationFragment::class.simpleName - } +class SettingsNotificationFragment : Fragment(R.layout.fragment_settings_notifications) { private val settingsViewModel: SettingsViewModel by activityViewModels() - private var binding: FragmentSettingsNotificationsBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingsNotificationsBinding.inflate(inflater) - binding.settingsViewModel = settingsViewModel - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentSettingsNotificationsBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.settingsViewModel = settingsViewModel setButtonOnClickListener() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt index bea672e29d7244f9a346c3f99a9af9a7ae3ba9a7..74bf4859462a9971738756c3583c3ad76a96a30c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt @@ -2,9 +2,7 @@ package de.rki.coronawarnapp.ui.settings import android.app.AlertDialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -16,9 +14,9 @@ import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.util.DataRetentionHelper import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,22 +26,13 @@ import kotlinx.coroutines.withContext * The user is informed what a reset means and he can perform it. * */ -class SettingsResetFragment : Fragment() { +class SettingsResetFragment : Fragment(R.layout.fragment_settings_reset) { companion object { private val TAG: String? = SettingsResetFragment::class.simpleName } - private var binding: FragmentSettingsResetBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingsResetBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentSettingsResetBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt index 702eebc1705fb16a1a272293323e4d3469e621e6..fef4caab8e27b670b2590a0116ab71de215eb934 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt @@ -2,9 +2,7 @@ package de.rki.coronawarnapp.ui.settings import android.content.Intent import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -17,13 +15,13 @@ import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.ExternalActionHelper import de.rki.coronawarnapp.util.IGNORE_CHANGE_TAG import de.rki.coronawarnapp.util.formatter.formatTracingSwitchEnabled +import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.launch @@ -35,7 +33,7 @@ import kotlinx.coroutines.launch * @see InternalExposureNotificationClient * @see InternalExposureNotificationPermissionHelper */ -class SettingsTracingFragment : Fragment(), +class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), InternalExposureNotificationPermissionHelper.Callback { companion object { @@ -44,24 +42,14 @@ class SettingsTracingFragment : Fragment(), private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() - private var binding: FragmentSettingsTracingBinding by viewLifecycle() + private val binding: FragmentSettingsTracingBinding by viewBindingLazy() private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSettingsTracingBinding.inflate(inflater) - binding.tracingViewModel = tracingViewModel - binding.settingsViewModel = settingsViewModel - binding.lifecycleOwner = this - return binding.root - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.tracingViewModel = tracingViewModel + binding.settingsViewModel = settingsViewModel setButtonOnClickListener() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt index 222aedad14e4c730216b13ca3c194d96a61352fd..85b74e67c5c4b6010dc4e37002fed6e21c7f1e74 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController @@ -11,25 +9,15 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionContactBinding import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * The [SubmissionContactFragment] allows requesting a teletan via phone */ -class SubmissionContactFragment : Fragment() { +class SubmissionContactFragment : Fragment(R.layout.fragment_submission_contact) { - private var binding: FragmentSubmissionContactBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionContactBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentSubmissionContactBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt index 1a82f3fb8de31c892773a64cc5f5102aaf225ba1..128285d437cf68c4bb4fa31752120117d040fcec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController @@ -11,26 +9,12 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionDispatcherBinding import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.ui.viewBindingLazy -class SubmissionDispatcherFragment : Fragment() { +class SubmissionDispatcherFragment : Fragment(R.layout.fragment_submission_dispatcher) { - companion object { - private val TAG: String? = SubmissionDispatcherFragment::class.simpleName - } - - private var binding: FragmentSubmissionDispatcherBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSubmissionDispatcherBinding.inflate(inflater) - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentSubmissionDispatcherBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDoneFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDoneFragment.kt index b76808583a8c3aadf49ca3b1b17469aeb72c4198..c8ebbb8a02ebd71d66e335427000101c60fb6184 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDoneFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDoneFragment.kt @@ -1,32 +1,22 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionDoneBinding import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * The [SubmissionDoneFragment] displays information to a user that submitted his exposure keys */ -class SubmissionDoneFragment : Fragment() { +class SubmissionDoneFragment : Fragment(R.layout.fragment_submission_done) { - private var binding: FragmentSubmissionDoneBinding by viewLifecycle() + private val binding: FragmentSubmissionDoneBinding by viewBindingLazy() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionDoneBinding.inflate(inflater) - return binding.root - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionIntroFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionIntroFragment.kt index e44e543222167e1fce9c3705e3d2a6d6de731684..cf00793bd02a96a8d4449a7a8df96af0a69a040d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionIntroFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionIntroFragment.kt @@ -1,32 +1,21 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionIntroBinding import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * The [SubmissionIntroFragment] displays information about how the corona warning system works */ -class SubmissionIntroFragment : Fragment() { +class SubmissionIntroFragment : Fragment(R.layout.fragment_submission_intro) { - private var binding: FragmentSubmissionIntroBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionIntroBinding.inflate(inflater) - return binding.root - } + private val binding: FragmentSubmissionIntroBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionQRCodeScanFragment.kt index 1542ea6998e8cbc47a7511bb72fea6e97624db5c..395769997db4c0b9672fd3d83ecb0b1b684a1add 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionQRCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionQRCodeScanFragment.kt @@ -3,9 +3,7 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.Manifest import android.content.pm.PackageManager import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -23,36 +21,25 @@ import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.util.CameraPermissionHelper import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.observeEvent +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * A simple [Fragment] subclass. */ -class SubmissionQRCodeScanFragment : Fragment() { +class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_code_scan) { companion object { private const val REQUEST_CAMERA_PERMISSION_CODE = 1 - private val TAG: String? = SubmissionQRCodeScanFragment::class.simpleName } private val viewModel: SubmissionViewModel by activityViewModels() - private var binding: FragmentSubmissionQrCodeScanBinding by viewLifecycle() + private val binding: FragmentSubmissionQrCodeScanBinding by viewBindingLazy() private var showsPermissionDialog = false - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSubmissionQrCodeScanBinding.inflate(inflater) - binding.lifecycleOwner = this - return binding.root - } - private fun decodeCallback(result: BarcodeResult) { viewModel.validateAndStoreTestGUID(result.text) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionResultPositiveOtherWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionResultPositiveOtherWarningFragment.kt index f62c30af37cc2ce91520fa0f2064b0ce261e8a14..5f3595ba3f003ad215c284aeb5174998ed3cbbf7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionResultPositiveOtherWarningFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionResultPositiveOtherWarningFragment.kt @@ -2,9 +2,7 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.content.Intent import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment @@ -21,23 +19,20 @@ import de.rki.coronawarnapp.exception.http.ForbiddenException import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.submission.ApiRequestState -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.observeEvent +import de.rki.coronawarnapp.util.ui.viewBindingLazy -class SubmissionResultPositiveOtherWarningFragment : Fragment(), +class SubmissionResultPositiveOtherWarningFragment : + Fragment(R.layout.fragment_submission_positive_other_warning), InternalExposureNotificationPermissionHelper.Callback { - companion object { - private val TAG: String? = SubmissionResultPositiveOtherWarningFragment::class.simpleName - } - private val submissionViewModel: SubmissionViewModel by activityViewModels() private val tracingViewModel: TracingViewModel by activityViewModels() - private var binding: FragmentSubmissionPositiveOtherWarningBinding by viewLifecycle() + private val binding: FragmentSubmissionPositiveOtherWarningBinding by viewBindingLazy() private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper @@ -55,20 +50,6 @@ class SubmissionResultPositiveOtherWarningFragment : Fragment(), tracingViewModel.refreshIsTracingEnabled() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - internalExposureNotificationPermissionHelper = - InternalExposureNotificationPermissionHelper(this, this) - binding = FragmentSubmissionPositiveOtherWarningBinding.inflate(inflater) - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) - binding.submissionViewModel = submissionViewModel - binding.lifecycleOwner = this - return binding.root - } - private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance { return when (exception) { is BadRequestException -> DialogHelper.DialogInstance( @@ -127,6 +108,12 @@ class SubmissionResultPositiveOtherWarningFragment : Fragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.submissionViewModel = submissionViewModel + + internalExposureNotificationPermissionHelper = + InternalExposureNotificationPermissionHelper(this, this) + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) + setButtonOnClickListener() submissionViewModel.submissionError.observeEvent(viewLifecycleOwner) { @@ -150,21 +137,23 @@ class SubmissionResultPositiveOtherWarningFragment : Fragment(), } /** - * Opens a Dialog that warns user - * when they're about to cancel the submission flow - * @see DialogHelper - * @see navigateToSubmissionResultFragment - */ + * Opens a Dialog that warns user + * when they're about to cancel the submission flow + * @see DialogHelper + * @see navigateToSubmissionResultFragment + */ fun handleSubmissionCancellation() { - DialogHelper.showDialog(DialogHelper.DialogInstance( - requireActivity(), - R.string.submission_error_dialog_confirm_cancellation_title, - R.string.submission_error_dialog_confirm_cancellation_body, - R.string.submission_error_dialog_confirm_cancellation_button_positive, - R.string.submission_error_dialog_confirm_cancellation_button_negative, - true, - ::navigateToSubmissionResultFragment - )) + DialogHelper.showDialog( + DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_confirm_cancellation_title, + R.string.submission_error_dialog_confirm_cancellation_body, + R.string.submission_error_dialog_confirm_cancellation_button_positive, + R.string.submission_error_dialog_confirm_cancellation_button_negative, + true, + ::navigateToSubmissionResultFragment + ) + ) } private fun navigateToSubmissionResultFragment() = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTanFragment.kt index bd8b681d6ebde3b71fb9abcc8240582bae1d3522..b078ad1f964985625e333fdeae8b0166fe800a09 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTanFragment.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -20,33 +18,21 @@ import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.TanConstants import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionTanViewModel -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.TanHelper import de.rki.coronawarnapp.util.observeEvent +import de.rki.coronawarnapp.util.ui.viewBindingLazy import kotlinx.android.synthetic.main.include_submission_tan.* /** * Fragment for TAN entry */ -class SubmissionTanFragment : Fragment() { +class SubmissionTanFragment : Fragment(R.layout.fragment_submission_tan) { private val viewModel: SubmissionTanViewModel by viewModels() private val submissionViewModel: SubmissionViewModel by activityViewModels() - private var binding: FragmentSubmissionTanBinding by viewLifecycle() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionTanBinding.inflate(inflater) - binding.viewmodel = viewModel - binding.lifecycleOwner = this - return binding.root - } + private val binding: FragmentSubmissionTanBinding by viewBindingLazy() private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance { return when (exception) { @@ -97,6 +83,7 @@ class SubmissionTanFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.viewmodel = viewModel binding.submissionTanContent.submissionTanInput.listener = { tan -> resetError() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTestResultFragment.kt index 3b316fa48ef13260e537ce7d58281891815976f4..cc109856925f49d24993b308bafea5a2bbe672a8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTestResultFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionTestResultFragment.kt @@ -2,9 +2,7 @@ package de.rki.coronawarnapp.ui.submission.fragment import android.app.AlertDialog import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment @@ -17,25 +15,22 @@ import de.rki.coronawarnapp.exception.http.CwaClientError import de.rki.coronawarnapp.exception.http.CwaServerError import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewLifecycle import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.observeEvent +import de.rki.coronawarnapp.util.ui.viewBindingLazy /** * A simple [Fragment] subclass. */ -class SubmissionTestResultFragment : Fragment() { - companion object { - private val TAG: String? = SubmissionTanFragment::class.simpleName - } +class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_result) { private val submissionViewModel: SubmissionViewModel by activityViewModels() private val tracingViewModel: TracingViewModel by activityViewModels() - private var binding: FragmentSubmissionTestResultBinding by viewLifecycle() + private val binding: FragmentSubmissionTestResultBinding by viewBindingLazy() private var skipInitialTestResultRefresh = false @@ -49,25 +44,6 @@ class SubmissionTestResultFragment : Fragment() { } } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionTestResultBinding.inflate(inflater) - binding.submissionViewModel = submissionViewModel - binding.lifecycleOwner = this - // registers callback when the os level back is pressed - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) - // Inflate the layout for this fragment - - skipInitialTestResultRefresh = - arguments?.getBoolean("skipInitialTestResultRefresh") ?: false - - return binding.root - } - private fun navigateToMainScreen() = findNavController().doNavigate( SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment() @@ -113,6 +89,13 @@ class SubmissionTestResultFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.submissionViewModel = submissionViewModel + // registers callback when the os level back is pressed + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) + + skipInitialTestResultRefresh = + arguments?.getBoolean("skipInitialTestResultRefresh") ?: false + setButtonOnClickListener() submissionViewModel.uiStateError.observeEvent(viewLifecycleOwner) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt index daa9dc01afa8314cedb3829fa1b60a27dfae31f1..6026460d18eb05d467d670825a6ccad8e9d531b7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppInjector.kt @@ -1,6 +1,14 @@ package de.rki.coronawarnapp.util.di +import android.app.Activity +import android.content.Context +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import dagger.android.AndroidInjection +import dagger.android.support.AndroidSupportInjection import de.rki.coronawarnapp.CoronaWarnApplication +import timber.log.Timber object AppInjector { lateinit var component: ApplicationComponent @@ -9,4 +17,32 @@ object AppInjector { component = DaggerApplicationComponent.factory().create(app) component.inject(app) } + + fun setup(activity: Activity) { + Timber.tag(TAG).d("Injecting %s", activity) + + // Using lifecycle callbacks would be even more awesome, + // but Activity.onPreCreate isn't available for our minAPI + AndroidInjection.inject(activity) + + if (activity is FragmentActivity) { + activity.supportFragmentManager + .registerFragmentLifecycleCallbacks(object : + FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentPreAttached( + fm: FragmentManager, + f: Fragment, + context: Context + ) { + if (f is AutoInject) { + Timber.tag(TAG).d("Injecting %s", f) + AndroidSupportInjection.inject(f) + } + super.onFragmentPreAttached(fm, f, context) + } + }, true) + } + } + + private val TAG = AppInjector::class.java.simpleName } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt index 2e6290343c50436e3cc4b809fcf3fcc6d9642725..03340dbfbb1ba4e43a4da4ed8f984fde8755b4c5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt @@ -5,6 +5,7 @@ import dagger.Component import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.nearby.NearbyModule import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader import de.rki.coronawarnapp.diagnosiskeys.server.AppConfigServer @@ -28,6 +29,7 @@ import javax.inject.Singleton @Component( modules = [ AndroidSupportInjectionModule::class, + AssistedInjectModule::class, AndroidModule::class, ReceiverBinder::class, ServiceBinder::class, @@ -36,7 +38,8 @@ import javax.inject.Singleton UtilModule::class, DeviceModule::class, HttpModule::class, - DiagnosisKeysModule::class + DiagnosisKeysModule::class, + NearbyModule::class ] ) interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AssistedInjectModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AssistedInjectModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..88cab114e805f64f0e01a1ab919de97c38232a83 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AssistedInjectModule.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.util.di + +import com.squareup.inject.assisted.dagger2.AssistedModule +import dagger.Module + +@AssistedModule +@Module(includes = [AssistedInject_AssistedInjectModule::class]) +interface AssistedInjectModule diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AutoInject.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AutoInject.kt new file mode 100644 index 0000000000000000000000000000000000000000..aea8f758de59fbee0f287d24bb6c27f0909e6f87 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AutoInject.kt @@ -0,0 +1,4 @@ +package de.rki.coronawarnapp.util.di + +// Annotating an Activity, Fragment marks it for injection via **[AppInjector]** +interface AutoInject diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ba82485f07bf8de8b8f829e99e450e487dcfb7d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.util.ui + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +fun <T> LiveData<T>.observe2(fragment: Fragment, callback: (T) -> Unit) { + observe(fragment.viewLifecycleOwner, Observer { callback.invoke(it) }) +} + +fun <T> LiveData<T>.observe2(activity: AppCompatActivity, callback: (T) -> Unit) { + observe(activity, Observer { callback.invoke(it) }) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SingleLiveEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SingleLiveEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..44809a1831ffe97fb8615ace28f1d692c173a421 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SingleLiveEvent.kt @@ -0,0 +1,71 @@ +package de.rki.coronawarnapp.util.ui + +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.annotation.MainThread +import androidx.annotation.Nullable +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A lifecycle-aware observable that sends only new updates after subscription, used for events like + * navigation and Snackbar messages. + * + * + * This avoids a common problem with events: on configuration change (like rotation) an update + * can be emitted if the observer is active. This LiveData only calls the observable if there's an + * explicit call to setValue() or call(). + * + * + * Note that only one observer is going to be notified of changes. + * https://github.com/android/architecture-samples/blob/166ca3a93ad14c6e224a3ea9bfcbd773eb048fb0/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java + */ +class SingleLiveEvent<T> : MutableLiveData<T>() { + + private val pending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { + if (hasActiveObservers()) { + Timber.w("Multiple observers registered but only one will be notified of changes.") + } + + // Observe the internal MutableLiveData + super.observe(owner, Observer<T> { t -> + if (pending.compareAndSet(true, false)) { + observer.onChanged(t) + } + }) + } + + @MainThread + override fun setValue(@Nullable t: T?) { + pending.set(true) + super.setValue(t) + } + + /** + * Used for cases where T is Void, to make calls cleaner. + */ + @MainThread + fun call() { + value = null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..0459511819f2e3bc7fc295e7c00ba8f1ac9230d0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewBindingExtensions.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.util.ui + +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.annotation.MainThread +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +inline fun <FragmentT : Fragment, reified BindingT : ViewBinding> FragmentT.viewBindingLazy() = + viewBindingLazy { + val bindingMethod = BindingT::class.java.getMethod("bind", View::class.java) + val binding = bindingMethod(null, requireView()) as BindingT + if (binding is ViewDataBinding) { + binding.lifecycleOwner = this + } + binding + } + +@Suppress("unused") +fun <FragmentT : Fragment, BindingT : ViewBinding> FragmentT.viewBindingLazy( + bindingProvider: FragmentT.() -> BindingT +) = ViewBindingProperty(bindingProvider) + +class ViewBindingProperty<ComponentT : LifecycleOwner, BindingT : ViewBinding>( + private val bindingProvider: (ComponentT) -> BindingT +) : ReadOnlyProperty<ComponentT, BindingT> { + + private val uiHandler = Handler(Looper.getMainLooper()) + private var localRef: ComponentT? = null + private var viewBinding: BindingT? = null + + private val onDestroyObserver = object : DefaultLifecycleObserver { + // Called right before Fragment.onDestroyView + override fun onDestroy(owner: LifecycleOwner) { + val ref = localRef ?: return + ref.lifecycle.removeObserver(this) + localRef = null + // Otherwise the binding is null before Fragment.onDestroyView + uiHandler.post { viewBinding = null } + } + } + + @MainThread + override fun getValue(thisRef: ComponentT, property: KProperty<*>): BindingT { + viewBinding?.let { + // Only accessible from within the same component + require(localRef === thisRef) + return@getValue it + } + + localRef = thisRef + thisRef.lifecycle.addObserver(onDestroyObserver) + + return bindingProvider(thisRef).also { viewBinding = it } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..065a6bc2bf629d194c66dbb9976fd70776ba10b7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.util.viewmodel + +import androidx.annotation.CallSuper +import androidx.lifecycle.ViewModel +import timber.log.Timber + +abstract class CWAViewModel : ViewModel() { + + init { + Timber.v("Initialized") + } + + @CallSuper + override fun onCleared() { + Timber.v("onCleared()") + super.onCleared() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ff5fa783d48b544b7b8974ca766e113e78c4495 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelExtensions.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.util.viewmodel + +import androidx.activity.ComponentActivity +import androidx.annotation.MainThread +import androidx.fragment.app.Fragment +import androidx.lifecycle.SavedStateHandle + +@MainThread +inline fun <reified VM : CWAViewModel> Fragment.cwaViewModels( + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory) +) = this.cwaViewModels<VM>(null, factoryProducer) + +@MainThread +inline fun <reified VM : CWAViewModel> Fragment.cwaViewModels( + noinline keyProducer: (() -> String)? = null, + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory) +) = viewModelsKeyed<VM>(keyProducer) { factoryProducer.invoke().create(this, arguments) } + +@MainThread +inline fun <reified VM : CWAViewModel> Fragment.cwaViewModelsAssisted( + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory), + noinline constructorCall: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel) +) = this.cwaViewModelsAssisted<VM>(null, factoryProducer, constructorCall) + +@MainThread +inline fun <reified VM : CWAViewModel> Fragment.cwaViewModelsAssisted( + noinline keyProducer: (() -> String)? = null, + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory), + noinline constructorCall: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel) +) = viewModelsKeyed<VM>(keyProducer) { + factoryProducer.invoke().create(this, arguments, constructorCall) +} + +@MainThread +inline fun <reified VM : CWAViewModel> ComponentActivity.cwaViewModels( + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory) +) = this.cwaViewModels<VM>(null, factoryProducer) + +@MainThread +inline fun <reified VM : CWAViewModel> ComponentActivity.cwaViewModels( + noinline keyProducer: (() -> String)? = null, + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory) +) = viewModelsKeyed<VM>(keyProducer) { factoryProducer.invoke().create(this, intent.extras) } + +@MainThread +inline fun <reified VM : CWAViewModel> ComponentActivity.cwaViewModelsAssisted( + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory), + noinline constructorCall: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel) +) = this.cwaViewModelsAssisted<VM>(null, factoryProducer, constructorCall) + +@MainThread +inline fun <reified VM : CWAViewModel> ComponentActivity.cwaViewModelsAssisted( + noinline keyProducer: (() -> String)? = null, + noinline factoryProducer: (() -> CWAViewModelFactoryProvider.Factory), + noinline constructorCall: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel) +) = viewModelsKeyed<VM>(keyProducer) { + factoryProducer.invoke().create(this, intent.extras, constructorCall) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc2241b0f0f24e060a6b5dbed45255c566cf345f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelFactory.kt @@ -0,0 +1,3 @@ +package de.rki.coronawarnapp.util.viewmodel + +interface CWAViewModelFactory<T : CWAViewModel> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelFactoryProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelFactoryProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..836fe2688145ef18cd97a9a24894d686e1ce0a86 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelFactoryProvider.kt @@ -0,0 +1,80 @@ +package de.rki.coronawarnapp.util.viewmodel + +import android.os.Bundle +import androidx.lifecycle.AbstractSavedStateViewModelFactory +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.savedstate.SavedStateRegistryOwner +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject + +/** + * Using fragments as example: + * + * Step 1: Inject CWAViewModelSource.Factory into Fragment + * + * Step 2: Use assisted inject `CWAViewModelSource.Factory.create(...)` to create an instance of + * CWAViewModelSource that has access to the fragments `savedStateOwner` + * + * Step 3: On first access (like "by lazy") to the viewModel the extension function, + * i.e. `ourViewModel.onActionClicked`, + * `cwaViewModelsAssisted` (or one of it's variants) checks the fragments viewmodel store + * + * Step 4: If no viewmodel was available, this factory is tasked with creating a new viewmodel + * + * Step 5: `CWAViewModelSource.create` is called as part of that flow and returns a new viewmodel + * + * Step 6: The new viewmodel is cached and returned + * + * Step 7: Finally whatever called `ourViewModel.onActionClicked` can execute. + */ +class CWAViewModelFactoryProvider @AssistedInject constructor( + private val creators: @JvmSuppressWildcards Map<Class<out CWAViewModel>, CWAViewModelFactory<out CWAViewModel>>, + @Assisted savedStateOwner: SavedStateRegistryOwner, + @Assisted defaultSavedState: Bundle?, + @Assisted private val assistAction: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel)? +) : AbstractSavedStateViewModelFactory(savedStateOwner, defaultSavedState) { + + /** + * Called indirectly (lazy) when the viewModel is accessed in a Fragment/Activity + **/ + override fun <T : ViewModel?> create( + key: String, + modelClass: Class<T>, + handle: SavedStateHandle + ): T { + // Find the right factory for our ViewModel + val factory = creators.entries.find { modelClass.isAssignableFrom(it.key) }?.value + if (factory == null) throw IllegalStateException("Unknown ViewModel factory: $modelClass") + + // If an `assistAction` was passed from `cwaViewModels` use that to create the ViewModel + @Suppress("UNCHECKED_CAST") + var vm: T? = assistAction?.invoke(factory, handle) as? T + + // If no `assistAction` was passed, try one of the defaults + // The Fragment or Activity may have used one of them to reduce boilerplate code. + @Suppress("UNCHECKED_CAST") + if (vm == null) { + vm = when (factory) { + is SavedStateCWAViewModelFactory<*> -> factory.create(handle) as? T + is SimpleCWAViewModelFactory<*> -> factory.create() as? T + else -> throw IllegalStateException("Unknown factory: $factory") + } + } + + return vm ?: throw IllegalStateException("$factory didn't return a ViewModel") + } + + /** + * Injected into fragments/activities + * Uses assisted injection to get access to the fragments/activities SavedStateRegistryOwner + */ + @AssistedInject.Factory + interface Factory { + fun create( + savedStateOwner: SavedStateRegistryOwner, + defaultSavedState: Bundle?, + assistAction: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel)? = null + ): CWAViewModelFactoryProvider + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelKey.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..f50cc6ccbcb44c789e5dd55d9f486c57603460b3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModelKey.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.util.viewmodel + +import dagger.MapKey +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +internal annotation class CWAViewModelKey(val value: KClass<out CWAViewModel>) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/SavedStateCWAViewModelFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/SavedStateCWAViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..add9ff8d2412d2d54fa37b00a6ded86c6ce9372f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/SavedStateCWAViewModelFactory.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.util.viewmodel + +import androidx.lifecycle.SavedStateHandle + +interface SavedStateCWAViewModelFactory<T : CWAViewModel> : CWAViewModelFactory<T> { + fun create(handle: SavedStateHandle): T +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/SimpleCWAViewModelFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/SimpleCWAViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..2998e5edd04a08fb06946934e3e12e3992e428ea --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/SimpleCWAViewModelFactory.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.util.viewmodel + +interface SimpleCWAViewModelFactory<T : CWAViewModel> : CWAViewModelFactory<T> { + fun create(): T +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/ViewModelLazyKeyed.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/ViewModelLazyKeyed.kt new file mode 100644 index 0000000000000000000000000000000000000000..11ea914bd437aa39d411728545bdc2e9000e825b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/ViewModelLazyKeyed.kt @@ -0,0 +1,184 @@ +package de.rki.coronawarnapp.util.viewmodel + +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.activity.ComponentActivity +import androidx.annotation.MainThread +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelLazy +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import kotlin.reflect.KClass + +/** + * This is a fork of the standard ViewModel code that allows to optionally provide + * alternative keys for ViewModels, to allow for more freedom when using multiple viewModels. + * Note the `keyProducer` argument. + */ + +/** + * An implementation of [Lazy] used by [androidx.fragment.app.Fragment.viewModels] and + * [androidx.activity.ComponentActivity.viewmodels]. + * + * [storeProducer] is a lambda that will be called during initialization, [VM] will be created + * in the scope of returned [ViewModelStore]. + * + * [factoryProducer] is a lambda that will be called during initialization, + * returned [ViewModelProvider.Factory] will be used for creation of [VM] + */ +class ViewModelLazyKeyed<VM : ViewModel>( + private val viewModelClass: KClass<VM>, + private val keyProducer: (() -> String)? = null, + private val storeProducer: () -> ViewModelStore, + private val factoryProducer: () -> ViewModelProvider.Factory +) : Lazy<VM> { + private var cached: VM? = null + + override val value: VM + get() { + val viewModel = cached + return if (viewModel == null) { + val factory = factoryProducer() + val store = storeProducer() + val key = keyProducer?.invoke() ?: "androidx.lifecycle.ViewModelProvider.DefaultKey" + ViewModelProvider(store, factory).get( + key + ":" + viewModelClass.java.canonicalName, + viewModelClass.java + ).also { + cached = it + } + } else { + viewModel + } + } + + override fun isInitialized() = cached != null +} + +/** + * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]: + * ``` + * class MyFragment : Fragment() { + * val viewmodel: NYViewModel by viewmodels() + * } + * ``` + * + * Custom [ViewModelProvider.Factory] can be defined via [factoryProducer] parameter, + * factory returned by it will be used to create [ViewModel]: + * ``` + * class MyFragment : Fragment() { + * val viewmodel: MYViewModel by viewmodels { myFactory } + * } + * ``` + * + * Default scope may be overridden with parameter [ownerProducer]: + * ``` + * class MyFragment : Fragment() { + * val viewmodel: MYViewModel by viewmodels ({requireParentFragment()}) + * } + * ``` + * + * This property can be accessed only after this Fragment is attached i.e., after + * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException. + */ +@MainThread +inline fun <reified VM : ViewModel> Fragment.viewModelsKeyed( + noinline keyProducer: (() -> String)? = null, + noinline ownerProducer: () -> ViewModelStoreOwner = { this }, + noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null +) = createViewModelLazyKeyed( + VM::class, + keyProducer, + { ownerProducer().viewModelStore }, + factoryProducer +) + +/** + * Returns a property delegate to access parent activity's [ViewModel], + * if [factoryProducer] is specified then [ViewModelProvider.Factory] + * returned by it will be used to create [ViewModel] first time. + * + * ``` + * class MyFragment : Fragment() { + * val viewmodel: MyViewModel by activityViewModels() + * } + * ``` + * + * This property can be accessed only after this Fragment is attached i.e., after + * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException. + */ +@MainThread +inline fun <reified VM : ViewModel> Fragment.activityViewModelsKeyed( + noinline keyProducer: (() -> String)? = null, + noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null +) = createViewModelLazyKeyed( + VM::class, + keyProducer, + { requireActivity().viewModelStore }, + factoryProducer +) + +/** + * Helper method for creation of [ViewModelLazy], that resolves `null` passed as [factoryProducer] + * to default factory. + */ +@MainThread +fun <VM : ViewModel> Fragment.createViewModelLazyKeyed( + viewModelClass: KClass<VM>, + keyProducer: (() -> String)? = null, + storeProducer: () -> ViewModelStore, + factoryProducer: (() -> ViewModelProvider.Factory)? = null +): Lazy<VM> { + val factoryPromise = factoryProducer ?: { + val application = activity?.application ?: throw IllegalStateException( + "ViewModel can be accessed only when Fragment is attached" + ) + ViewModelProvider.AndroidViewModelFactory.getInstance(application) + } + return ViewModelLazyKeyed(viewModelClass, keyProducer, storeProducer, factoryPromise) +} + +/** + * Returns a [Lazy] delegate to access the ComponentActivity's ViewModel, if [factoryProducer] + * is specified then [ViewModelProvider.Factory] returned by it will be used + * to create [ViewModel] first time. + * + * ``` + * class MyComponentActivity : ComponentActivity() { + * val viewmodel: MyViewModel by viewmodels() + * } + * ``` + * + * This property can be accessed only after the Activity is attached to the Application, + * and access prior to that will result in IllegalArgumentException. + */ +@MainThread +inline fun <reified VM : ViewModel> ComponentActivity.viewModelsKeyed( + noinline keyProducer: (() -> String)? = null, + noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null +): Lazy<VM> { + val factoryPromise = factoryProducer ?: { + val application = application ?: throw IllegalArgumentException( + "ViewModel can be accessed only when Activity is attached" + ) + ViewModelProvider.AndroidViewModelFactory.getInstance(application) + } + + return ViewModelLazyKeyed(VM::class, keyProducer, { viewModelStore }, factoryPromise) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyFileDownloaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyFileDownloaderTest.kt index d9df9734b50b60a323366f41c3cb9de7ef39ea2c..4557883674743c15d6c6545486aefce57a5d90b4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyFileDownloaderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyFileDownloaderTest.kt @@ -62,6 +62,8 @@ class KeyFileDownloaderTest : BaseIOTest() { testDir.exists() shouldBe true mockkObject(CWADebug) + every { CWADebug.isDebugBuildOrMode } returns false + every { settings.isLast3HourModeEnabled } returns false coEvery { diagnosisKeyServer.getCountryIndex() } returns listOf( LocationCode("DE"), @@ -71,9 +73,6 @@ class KeyFileDownloaderTest : BaseIOTest() { every { isSpaceAvailable } returns true } - coEvery { settings.isLast3HourModeEnabled } returns false - - coEvery { diagnosisKeyServer.getCountryIndex() } returns listOf( LocationCode("DE"), LocationCode("NL") ) diff --git a/Corona-Warn-App/src/test/java/testhelpers/extensions/CoroutineTestExtension.kt b/Corona-Warn-App/src/test/java/testhelpers/extensions/CoroutineTestExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2180a1c80b9363e296fbaefb538d1aca762a008 --- /dev/null +++ b/Corona-Warn-App/src/test/java/testhelpers/extensions/CoroutineTestExtension.kt @@ -0,0 +1,26 @@ +package testhelpers.extensions + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext + +@ExperimentalCoroutinesApi +class CoroutinesTestExtension( + private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() +) : BeforeEachCallback, AfterEachCallback, TestCoroutineScope by TestCoroutineScope(dispatcher) { + + override fun beforeEach(context: ExtensionContext?) { + Dispatchers.setMain(dispatcher) + } + + override fun afterEach(context: ExtensionContext?) { + cleanupTestCoroutines() + Dispatchers.resetMain() + } +} diff --git a/Corona-Warn-App/src/test/java/testhelpers/extensions/InstantExecutorExtension.kt b/Corona-Warn-App/src/test/java/testhelpers/extensions/InstantExecutorExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f9ab64108ff01f43cb7363786a094454ac1f02f --- /dev/null +++ b/Corona-Warn-App/src/test/java/testhelpers/extensions/InstantExecutorExtension.kt @@ -0,0 +1,25 @@ +package testhelpers.extensions + +import androidx.arch.core.executor.ArchTaskExecutor +import androidx.arch.core.executor.TaskExecutor +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext + +class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback { + + override fun beforeEach(context: ExtensionContext?) { + ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) = runnable.run() + + override fun postToMainThread(runnable: Runnable) = runnable.run() + + override fun isMainThread(): Boolean = true + }) + } + + override fun afterEach(context: ExtensionContext?) { + ArchTaskExecutor.getInstance().setDelegate(null) + } + +} diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f04e64c371d04d92ade5ef94b9dbcab8ca59c4a --- /dev/null +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt @@ -0,0 +1,103 @@ +package de.rki.coronawarnapp.test.risklevel.ui + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.transaction.RiskLevelTransaction +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.impl.annotations.MockK +import io.mockk.mockkObject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.extensions.CoroutinesTestExtension +import testhelpers.extensions.InstantExecutorExtension + +@ExperimentalCoroutinesApi +@ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class) +class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() { + + @MockK lateinit var context: Context + @MockK lateinit var savedStateHandle: SavedStateHandle + @MockK lateinit var exposureNotificationClient: ExposureNotificationClient + @MockK lateinit var keyCacheRepository: KeyCacheRepository + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkObject(RetrieveDiagnosisKeysTransaction) + coEvery { RetrieveDiagnosisKeysTransaction.start() } returns Unit + mockkObject(RiskLevelTransaction) + coEvery { RiskLevelTransaction.start() } returns Unit + + coEvery { keyCacheRepository.clear() } returns Unit + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createViewModel(exampleArgs: String? = null): TestRiskLevelCalculationFragmentCWAViewModel = + TestRiskLevelCalculationFragmentCWAViewModel( + handle = savedStateHandle, + exampleArg = exampleArgs, + context = context, + exposureNotificationClient = exposureNotificationClient, + keyCacheRepository = keyCacheRepository + ) + + @Test + fun `action retrieveDiagnosisKeys, retieves diagnosis keys and calls risklevel calculation`() { + val vm = createViewModel() + + vm.retrieveDiagnosisKeys() + + coVerifyOrder { + RetrieveDiagnosisKeysTransaction.start() + RiskLevelTransaction.start() + } + } + + @Test + fun `action calculateRiskLevel, calls risklevel calculation`() { + val vm = createViewModel() + + vm.calculateRiskLevel() + + coVerify(exactly = 1) { RiskLevelTransaction.start() } + coVerify(exactly = 0) { RetrieveDiagnosisKeysTransaction.start() } + } + + @Test + fun `action clearDiagnosisKeys calls the keyCacheRepo`() { + val vm = createViewModel() + + vm.clearKeyCache() + + coVerify(exactly = 1) { keyCacheRepository.clear() } + } + + @Test + fun `action scanLocalQRCodeAndProvide, triggers event`() { + val vm = createViewModel() + + vm.startLocalQRCodeScanEvent.value shouldBe null + + vm.scanLocalQRCodeAndProvide() + + vm.startLocalQRCodeScanEvent.value shouldBe Unit + } + +} diff --git a/build.gradle b/build.gradle index 9204a9f1ce135bb448852f56d08cf2eec84a33c5..3758485c638c3420912fe1f8c8a0afefebdc3773 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.4.0' ext.protobufVersion = '0.8.12' ext.navVersion = "2.2.2"