diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index ad0a14fdcb2..4e2526202a1 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -33,19 +33,23 @@ android { applicationId 'de.rki.coronawarnapp' minSdkVersion 23 targetSdkVersion 29 - versionCode 9 - versionName "0.8.2" + versionCode 10 + versionName "0.8.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "DOWNLOAD_CDN_URL", "\"$DOWNLOAD_CDN_URL\"" buildConfigField "String", "SUBMISSION_CDN_URL", "\"$SUBMISSION_CDN_URL\"" buildConfigField "String", "VERIFICATION_CDN_URL", "\"$VERIFICATION_CDN_URL\"" + buildConfigField "String", "TRUSTED_CERTS_EXPORT_KEYSTORE_PW", "\"$TRUSTED_CERTS_EXPORT_KEYSTORE_PW\"" //override URLs with local variables Properties properties = new Properties() def propertiesFile = project.rootProject.file('local.properties') if (propertiesFile.exists()) { properties.load(propertiesFile.newDataInputStream()) + def secretFile = project.rootProject.file('secrets.properties') + if (secretFile.exists()) + properties.load(secretFile.newDataInputStream()) def DOWNLOAD_CDN_URL = properties.getProperty('DOWNLOAD_CDN_URL') if (DOWNLOAD_CDN_URL) @@ -58,6 +62,10 @@ android { def VERIFICATION_CDN_URL = properties.getProperty('VERIFICATION_CDN_URL') if (VERIFICATION_CDN_URL) buildConfigField "String", "VERIFICATION_CDN_URL", "\"$VERIFICATION_CDN_URL\"" + + def TRUSTED_CERTS_EXPORT_KEYSTORE_PW = properties.getProperty('TRUSTED_CERTS_EXPORT_KEYSTORE_PW') + if (TRUSTED_CERTS_EXPORT_KEYSTORE_PW) + buildConfigField "String", "TRUSTED_CERTS_EXPORT_KEYSTORE_PW", "\"$TRUSTED_CERTS_EXPORT_KEYSTORE_PW\"" } } diff --git a/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks b/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks new file mode 100644 index 00000000000..e67a6926a60 Binary files /dev/null and b/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks differ 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 c41efeec1fc..bf6dd5608af 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 @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Application import android.content.Context +import android.content.IntentFilter import android.content.pm.ActivityInfo import android.os.Bundle import android.util.Log @@ -12,6 +13,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import de.rki.coronawarnapp.exception.ErrorReportReceiver +import de.rki.coronawarnapp.exception.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL +import de.rki.coronawarnapp.exception.handler.GlobalExceptionHandler import de.rki.coronawarnapp.notification.NotificationHelper import org.conscrypt.Conscrypt import java.security.Security @@ -34,12 +39,15 @@ class CoronaWarnApplication : Application(), LifecycleObserver, instance.applicationContext } + private lateinit var errorReceiver: ErrorReportReceiver + override fun onCreate() { + super.onCreate() + GlobalExceptionHandler(this) instance = this NotificationHelper.createNotificationChannel() // Enable Conscrypt for TLS1.3 Support below API Level 29 Security.insertProviderAt(Conscrypt.newProvider(), 1) - super.onCreate() ProcessLifecycleOwner.get().lifecycle.addObserver(this) registerActivityLifecycleCallbacks(this) } @@ -63,7 +71,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver, } override fun onActivityPaused(activity: Activity) { - // does not override function. Empty on intention + // unregisters error receiver + LocalBroadcastManager.getInstance(this).unregisterReceiver(errorReceiver) } override fun onActivityStarted(activity: Activity) { @@ -94,6 +103,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver, } override fun onActivityResumed(activity: Activity) { - // does not override function. Empty on intention + errorReceiver = ErrorReportReceiver(activity) + LocalBroadcastManager.getInstance(this) + .registerReceiver(errorReceiver, IntentFilter(ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt index e755d25ccb3..8a2247446f5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt @@ -2,17 +2,23 @@ package de.rki.coronawarnapp import android.content.Intent import android.graphics.Bitmap +import android.graphics.Color import android.os.Bundle +import android.util.Base64 import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.Switch import android.widget.Toast import androidx.core.content.pm.PackageInfoCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient @@ -20,8 +26,11 @@ import com.google.android.gms.nearby.exposurenotification.ExposureSummary import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import com.google.protobuf.ByteString +import com.google.zxing.BarcodeFormat import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentResult +import com.google.zxing.qrcode.QRCodeWriter import de.rki.coronawarnapp.databinding.FragmentTestForAPIBinding import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL @@ -55,7 +64,6 @@ import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.button_insert_expo import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.button_retrieve_exposure_summary import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.button_tracing_duration_in_retention_period import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.button_tracing_intervals -import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.image_qr_code import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_summary_attenuation import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_summary_daysSinceLastExposure import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_summary_matchedKeyCount @@ -63,6 +71,8 @@ import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_sum import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_summary_summationRiskScore import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_googlePlayServices_version import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_my_keys +import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.qr_code_viewpager +import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.test_api_switch_last_three_hours_from_server import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.text_my_keys import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.text_scanned_key import kotlinx.coroutines.Dispatchers @@ -91,6 +101,7 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel } private var myExposureKeysJSON: String? = null + private var myExposureKeys: List? = mutableListOf() private var otherExposureKey: AppleLegacyKeyExchange.Key? = null private var otherExposureKeyList = mutableListOf() @@ -101,8 +112,12 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel // ViewModel for MainActivity private val tracingViewModel: TracingViewModel by activityViewModels() + private lateinit var qrPager: ViewPager2 + private lateinit var qrPagerAdapter: RecyclerView.Adapter + // Data and View binding - private lateinit var binding: FragmentTestForAPIBinding + private var _binding: FragmentTestForAPIBinding? = null + private val binding: FragmentTestForAPIBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -111,7 +126,7 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentTestForAPIBinding.inflate(inflater) + _binding = FragmentTestForAPIBinding.inflate(inflater) // set the viewmmodel variable that will be used for data binding binding.tracingViewModel = tracingViewModel @@ -123,6 +138,11 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -139,15 +159,24 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel internalExposureNotificationPermissionHelper = InternalExposureNotificationPermissionHelper(this, this) + qrPager = qr_code_viewpager + qrPagerAdapter = QRPagerAdapter() + qrPager.adapter = qrPagerAdapter + button_api_test_start.setOnClickListener { start() } button_api_get_exposure_keys.setOnClickListener { getExposureKeys() - tracingViewModel.viewModelScope.launch { - ExposureSharingService.shareKeysAsBitmap(300, 300, updateQRImageView) - } + } + + val last3HoursSwitch = test_api_switch_last_three_hours_from_server as Switch + last3HoursSwitch.isChecked = LocalData.last3HoursMode() + last3HoursSwitch.setOnClickListener { + val isLast3HoursModeEnabled = last3HoursSwitch.isChecked + showToast("Last 3 Hours Mode is activated: $isLast3HoursModeEnabled") + LocalData.last3HoursMode(isLast3HoursModeEnabled) } button_api_get_check_exposure.setOnClickListener { @@ -241,13 +270,6 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel getExposureKeys() } - private val updateQRImageView = { bitmap: Bitmap? -> - bitmap?.let { - image_qr_code.setImageBitmap(bitmap) - image_qr_code.visibility = View.VISIBLE - } - } - private val prettyKey = { key: AppleLegacyKeyExchange.Key -> StringBuilder() .append("\nKey data: ${key.keyData}") @@ -423,10 +445,6 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel ) label_my_keys.text = myKeysLabelAndCount text_my_keys.text = myExposureKeysJSON - - tracingViewModel.viewModelScope.launch { - ExposureSharingService.shareKeysAsBitmap(300, 300, updateQRImageView) - } } private fun showToast(message: String) { @@ -445,6 +463,9 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel override fun onKeySharePermissionGranted(keys: List) { myExposureKeysJSON = keysToJson(keys) + myExposureKeys = keys + qrPagerAdapter.notifyDataSetChanged() + updateKeysDisplay() } @@ -491,4 +512,46 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel CONFIG_SCORE ) .build() + + private inner class QRPagerAdapter : + RecyclerView.Adapter() { + + inner class QRViewHolder(val qrCode: ImageView) : RecyclerView.ViewHolder(qrCode) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QRViewHolder { + val imageView = LayoutInflater.from(parent.context) + .inflate(R.layout.test_qr_code_view, parent, false) as ImageView + return QRViewHolder(imageView) + } + + override fun getItemCount(): Int = myExposureKeys?.size ?: 0 + + override fun onBindViewHolder(holder: QRViewHolder, position: Int) { + myExposureKeys?.get(position)?.let { + holder.qrCode.setImageBitmap(bitmapForImage(it)) + } + } + + private fun bitmapForImage(key: TemporaryExposureKey): Bitmap { + val key = AppleLegacyKeyExchange.Key.newBuilder() + .setKeyData(ByteString.copyFrom(key.keyData)) + .setRollingPeriod(key.rollingPeriod) + .setRollingStartNumber(key.rollingStartIntervalNumber) + .build().toByteArray() + val bMatrix = QRCodeWriter().encode( + Base64.encodeToString(key, Base64.DEFAULT), + BarcodeFormat.QR_CODE, + 300, + 300 + ) + val bmp = + Bitmap.createBitmap(bMatrix.width, bMatrix.height, Bitmap.Config.RGB_565) + for (x in 0 until bMatrix.width) { + for (y in 0 until bMatrix.height) { + bmp.setPixel(x, y, if (bMatrix.get(x, y)) Color.BLACK else Color.WHITE) + } + } + return bmp + } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt index b1f9739c044..ef572b3a276 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt @@ -6,6 +6,7 @@ import android.util.Log 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 @@ -26,6 +27,8 @@ import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.sharing.ExposureSharingService +import de.rki.coronawarnapp.storage.AppDatabase +import de.rki.coronawarnapp.storage.FileStorageHelper import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction import de.rki.coronawarnapp.transaction.RiskLevelTransaction @@ -33,7 +36,10 @@ 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 kotlinx.android.synthetic.main.fragment_test_risk_level_calculation.transmission_number +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.util.UUID import java.util.concurrent.TimeUnit @@ -54,14 +60,15 @@ class TestRiskLevelCalculation : Fragment() { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() private val submissionViewModel: SubmissionViewModel by activityViewModels() - private lateinit var binding: FragmentTestRiskLevelCalculationBinding + private var _binding: FragmentTestRiskLevelCalculationBinding? = null + private val binding: FragmentTestRiskLevelCalculationBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentTestRiskLevelCalculationBinding.inflate(inflater) + _binding = FragmentTestRiskLevelCalculationBinding.inflate(inflater) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.submissionViewModel = submissionViewModel @@ -69,6 +76,11 @@ class TestRiskLevelCalculation : Fragment() { return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -88,6 +100,33 @@ class TestRiskLevelCalculation : Fragment() { } } + binding.buttonResetRiskLevel.setOnClickListener { + tracingViewModel.viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + // Database Reset + AppDatabase.getInstance(requireContext()).clearAllTables() + // Delete Database Instance + AppDatabase.resetInstance(requireContext()) + // Export File Reset + FileStorageHelper.getAllFilesInKeyExportDirectory().forEach { it.delete() } + + 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(), "Resetted, please fetch diagnosis keys from server again", + Toast.LENGTH_SHORT + ).show() + } + } + startObserving() } @@ -117,6 +156,7 @@ class TestRiskLevelCalculation : Fragment() { private suspend fun retrieveDiagnosisKeys() { try { RetrieveDiagnosisKeysTransaction.start() + calculateRiskLevel() } catch (e: TransactionException) { e.report(ExceptionCategory.INTERNAL) } @@ -144,12 +184,18 @@ class TestRiskLevelCalculation : Fragment() { val appleKeyList = mutableListOf() + 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(1) + .setTransmissionRiskLevel(number) .build() ) @@ -204,7 +250,10 @@ class TestRiskLevelCalculation : Fragment() { Observer { tracingViewModel.viewModelScope.launch { val riskAsString = "Level: ${it.riskLevel}\n" + - "Calc. Score: ${it.riskScore}\n" + + "Last successful Level: " + + "${LocalData.lastSuccessfullyCalculatedRiskLevel()}\n" + + "Calculated Score: ${it.riskScore}\n" + + "Last Time Server Fetch: ${LocalData.lastTimeDiagnosisKeysFromServerFetch()}\n" + "Tracing Duration: " + "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" + "Tracing Duration in last 14 days: " + diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt new file mode 100644 index 00000000000..93828953239 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.exception + +import java.lang.Exception + +class ApplicationConfigurationCorruptException : Exception( + "the application configuration is corrupt" +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt new file mode 100644 index 00000000000..73272e43de0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.exception + +class ApplicationConfigurationInvalidException : Exception( + "the application configuration is invalid" +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt new file mode 100644 index 00000000000..3e1c3efcbee --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.exception + +import java.lang.Exception + +class CwaSecurityException(cause: Throwable) : Exception( + "something went wrong during a critical part of the application ensuring security, please refer" + + "to the details for more information", + cause +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt index f9d52f4f917..958e8a1dd95 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt @@ -13,6 +13,7 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver() companion object { private val TAG: String = ErrorReportReceiver::class.java.simpleName } + override fun onReceive(context: Context, intent: Intent) { val category = ExceptionCategory .valueOf(intent.getStringExtra(ReportingConstants.ERROR_REPORT_CATEGORY_EXTRA) ?: "") @@ -25,25 +26,28 @@ class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver() val confirm = context.resources.getString(R.string.errors_generic_button_positive) val details = context.resources.getString(R.string.errors_generic_button_negative) val detailsTitle = context.resources.getString(R.string.errors_generic_details_headline) + if (CoronaWarnApplication.isAppInForeground) { - DialogHelper.showDialog(DialogHelper.DialogInstance( - activity, - title, - message, - confirm, - details, - null, - {}, - { - DialogHelper.showDialog( - DialogHelper.DialogInstance( - activity, - title, - "$detailsTitle:\n$stack", - confirm - )).run {} - } - )) + DialogHelper.showDialog( + DialogHelper.DialogInstance( + activity, + title, + message, + confirm, + details, + null, + {}, + { + DialogHelper.showDialog( + DialogHelper.DialogInstance( + activity, + title, + "$detailsTitle:\n$stack", + confirm + ) + ).run {} + } + )) } Log.e( TAG, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt index 65b1713f114..3c58048be14 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt @@ -26,7 +26,7 @@ fun Throwable.report( LocalBroadcastManager.getInstance(CoronaWarnApplication.getAppContext()).sendBroadcast(intent) } -fun Throwable.reportGeneric( +fun reportGeneric( stackString: String ) { val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/SubmissionTanInvalidException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/SubmissionTanInvalidException.kt new file mode 100644 index 00000000000..55b2acb7cde --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/SubmissionTanInvalidException.kt @@ -0,0 +1,28 @@ +/****************************************************************************** + * Corona-Warn-App * + * * + * SAP SE and all other contributors / * + * copyright owners license this file to you 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. * + ******************************************************************************/ + +package de.rki.coronawarnapp.exception + +/** + * An Exception thrown when an error occurs inside the [de.rki.coronawarnapp.http.WebRequestBuilder] + * + * @param message an Exception thrown inside the WebRequestBuilder + * @param cause the cause of the error + */ +class SubmissionTanInvalidException(message: String, cause: Throwable) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TestAlreadyPairedException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TestAlreadyPairedException.kt new file mode 100644 index 00000000000..b2af0b017dc --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TestAlreadyPairedException.kt @@ -0,0 +1,28 @@ +/****************************************************************************** + * Corona-Warn-App * + * * + * SAP SE and all other contributors / * + * copyright owners license this file to you 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. * + ******************************************************************************/ + +package de.rki.coronawarnapp.exception + +/** + * An Exception thrown when an error occurs inside the [de.rki.coronawarnapp.http.WebRequestBuilder] + * + * @param message an Exception thrown inside the WebRequestBuilder + * @param cause the cause of the error + */ +class TestAlreadyPairedException(message: String, cause: Throwable) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TestPairingInvalidException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TestPairingInvalidException.kt new file mode 100644 index 00000000000..77619440763 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/TestPairingInvalidException.kt @@ -0,0 +1,28 @@ +/****************************************************************************** + * Corona-Warn-App * + * * + * SAP SE and all other contributors / * + * copyright owners license this file to you 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. * + ******************************************************************************/ + +package de.rki.coronawarnapp.exception + +/** + * An Exception thrown when an error occurs inside the [de.rki.coronawarnapp.http.WebRequestBuilder] + * + * @param message an Exception thrown inside the WebRequestBuilder + * @param cause the cause of the error + */ +class TestPairingInvalidException(message: String, cause: Throwable) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/handler/GlobalExceptionHandler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/handler/GlobalExceptionHandler.kt new file mode 100644 index 00000000000..2fe17a4b61c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/handler/GlobalExceptionHandler.kt @@ -0,0 +1,68 @@ +package de.rki.coronawarnapp.exception.handler + +import android.content.Context +import android.content.Intent +import android.util.Log +import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.ui.LauncherActivity +import java.io.PrintWriter +import java.io.StringWriter +import java.lang.reflect.InvocationTargetException + +class GlobalExceptionHandler(private val application: CoronaWarnApplication) : + Thread.UncaughtExceptionHandler { + + companion object { + val TAG: String? = GlobalExceptionHandler::class.simpleName + } + + init { + Thread.setDefaultUncaughtExceptionHandler(this) + } + + override fun uncaughtException(thread: Thread, throwable: Throwable) { + try { + Log.i(TAG, "cause caught: " + throwable) + val cause = throwable.cause + + val stringWriter = StringWriter() + // Throwables from main thread are wrapped in an InvocationTargetException, + // unwrap the InvocationTargetException to get the original cause + if (cause is InvocationTargetException) { + cause.targetException.printStackTrace(PrintWriter(stringWriter)) + Log.i(TAG, "InvocationTargetException caught: " + cause.targetException) + } + // for errors thrown by coroutines, these are not wrapped in InvocationTargetException + else { + Log.i(TAG, "InvocationTargetException caught: " + throwable) + throwable.printStackTrace(PrintWriter(stringWriter)) + } + val stackTrace = stringWriter.toString() + triggerRestart(CoronaWarnApplication.getAppContext(), stackTrace) + } catch (e: Exception) { + Log.e(TAG, "GlobalExceptionHandler failing" + e) + } + } + + /** + * Restarts the app by sending an Intent to start LauncherActivitiy and + * terminating the JVM + * + * @see de.rki.coronawarnapp.ui.LauncherActivity + * + * @param context application context + * @param stackTrace exception that caused the crash + */ + private fun triggerRestart(context: Context, stackTrace: String) { + val intent = Intent(context, LauncherActivity::class.java) + intent.addFlags( + Intent.FLAG_ACTIVITY_CLEAR_TOP + or Intent.FLAG_ACTIVITY_CLEAR_TASK + or Intent.FLAG_ACTIVITY_NEW_TASK + ) + intent.putExtra(GlobalExceptionHandlerConstants.APP_CRASHED, true) + intent.putExtra(GlobalExceptionHandlerConstants.STACK_TRACE, stackTrace) + context.startActivity(intent) + Runtime.getRuntime().exit(0) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/handler/GlobalExceptionHandlerConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/handler/GlobalExceptionHandlerConstants.kt new file mode 100644 index 00000000000..3d0509123c7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/handler/GlobalExceptionHandlerConstants.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.exception.handler + +object GlobalExceptionHandlerConstants { + + // name of intent extra to described that an app has crashed. Intent extra is of type boolean + const val APP_CRASHED = "appCrashed" + + // name of intent extra that contains the stacktrace. Intent extra is of type boolean + const val STACK_TRACE = "stackTrace" +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt index a5e68d8526f..408169d263c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt @@ -21,6 +21,9 @@ package de.rki.coronawarnapp.http import KeyExportFormat import android.util.Log +import com.google.protobuf.InvalidProtocolBufferException +import de.rki.coronawarnapp.exception.ApplicationConfigurationCorruptException +import de.rki.coronawarnapp.exception.ApplicationConfigurationInvalidException import de.rki.coronawarnapp.http.requests.RegistrationTokenRequest import de.rki.coronawarnapp.http.requests.ReqistrationRequest import de.rki.coronawarnapp.http.requests.TanRequestBody @@ -40,6 +43,9 @@ import java.util.UUID object WebRequestBuilder { private val TAG: String? = WebRequestBuilder::class.simpleName + private const val EXPORT_BINARY_FILE_NAME = "export.bin" + private const val EXPORT_SIGNATURE_FILE_NAME = "export.sig" + private val serviceFactory = ServiceFactory() private val distributionService = serviceFactory.distributionService() @@ -81,25 +87,28 @@ object WebRequestBuilder { suspend fun asyncGetApplicationConfigurationFromServer(): ApplicationConfiguration = withContext(Dispatchers.IO) { - var applicationConfiguration: ApplicationConfiguration? = null + var exportBinary: ByteArray? = null + var exportSignature: ByteArray? = null + distributionService.getApplicationConfiguration( DiagnosisKeyConstants.COUNTRY_APPCONFIG_DOWNLOAD_URL ).byteStream().unzip { entry, entryContent -> - if (entry.name == "export.bin") { - val appConfig = ApplicationConfiguration.parseFrom(entryContent) - applicationConfiguration = appConfig - } - if (entry.name == "export.sig") { - val signatures = KeyExportFormat.TEKSignatureList.parseFrom(entryContent) - signatures.signaturesList.forEach { - Log.d(TAG, it.signatureInfo.toString()) - } - } + if (entry.name == EXPORT_BINARY_FILE_NAME) exportBinary = entryContent.copyOf() + if (entry.name == EXPORT_SIGNATURE_FILE_NAME) exportSignature = entryContent.copyOf() + } + if (exportBinary == null || exportSignature == null) { + throw ApplicationConfigurationInvalidException() } - if (applicationConfiguration == null) { - throw IllegalArgumentException("no file was found in the downloaded zip") + + if (!SecurityHelper.exportFileIsValid(exportBinary, exportSignature)) { + throw ApplicationConfigurationCorruptException() + } + + try { + return@withContext ApplicationConfiguration.parseFrom(exportBinary) + } catch (e: InvalidProtocolBufferException) { + throw ApplicationConfigurationInvalidException() } - return@withContext applicationConfiguration!! } suspend fun asyncGetRegistrationToken( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt index 0f521a9d188..68db8003d01 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt @@ -42,6 +42,12 @@ enum class RiskLevel(val raw: Int) { } // risk level categories + val UNSUCCESSFUL_RISK_LEVELS = + arrayOf( + UNDETERMINED, + NO_CALCULATION_POSSIBLE_TRACING_OFF, + UNKNOWN_RISK_OUTDATED_RESULTS + ) private val HIGH_RISK_LEVELS = arrayOf(INCREASED_RISK) private val LOW_RISK_LEVELS = arrayOf( UNKNOWN_RISK_INITIAL, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt index 2e90edd67d3..5a6537cc950 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt @@ -76,14 +76,14 @@ object TimeVariables { /** * The timeRange until the calculated exposure figures are rated as stale. - * In days. + * In hours. */ - private const val MAX_STALE_EXPOSURE_RISK_RANGE = 1 + private const val MAX_STALE_EXPOSURE_RISK_RANGE = 48 /** * Getter function for [MAX_STALE_EXPOSURE_RISK_RANGE] * - * @return stale threshold in days + * @return stale threshold in hours */ fun getMaxStaleExposureRiskRange(): Int = MAX_STALE_EXPOSURE_RISK_RANGE @@ -106,6 +106,7 @@ object TimeVariables { * Internal requirements: 2 hours = 7200000 milliseconds * Test value: 1 minute */ + // todo exchange with real value (currently 120 min) private const val MANUAL_KEY_RETRIEVAL_DELAY = 60000L /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt index beef2512531..f7a653b7268 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt @@ -77,4 +77,6 @@ object DiagnosisKeyConstants { /** Available Dates URL built from CDN URL's and REST resources */ val AVAILABLE_DATES_URL = "$DIAGNOSIS_KEYS_DOWNLOAD_URL/$COUNTRY/$CURRENT_COUNTRY/$DATE" + + const val SERVER_ERROR_CODE_403 = 403 } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt index 24682d27e24..e30de86ea56 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt @@ -23,7 +23,10 @@ import KeyExportFormat import android.util.Log import de.rki.coronawarnapp.exception.DiagnosisKeyRetrievalException import de.rki.coronawarnapp.exception.DiagnosisKeySubmissionException +import de.rki.coronawarnapp.exception.SubmissionTanInvalidException import de.rki.coronawarnapp.http.WebRequestBuilder +import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants.SERVER_ERROR_CODE_403 +import retrofit2.HttpException /** * The Diagnosis Key Service is used to interact with the Server to submit and retrieve keys through @@ -54,8 +57,14 @@ object DiagnosisKeyService { false, keysToReport ) - } catch (e: Exception) { - throw DiagnosisKeySubmissionException(e) + } catch (e: HttpException) { + if (e.code() == SERVER_ERROR_CODE_403) { + throw SubmissionTanInvalidException( + "the test paring to the device is invalid", + e + ) + } + throw e } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt index a99128c9013..4150a275b99 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt @@ -20,4 +20,6 @@ object SubmissionConstants { const val MAX_QR_CODE_LENGTH = 150 const val MAX_GUID_LENGTH = 80 const val GUID_SEPARATOR = '?' + + const val SERVER_ERROR_CODE_400 = 400 } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt index c50399d451d..fc3f3266758 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt @@ -2,24 +2,38 @@ package de.rki.coronawarnapp.service.submission import de.rki.coronawarnapp.exception.NoGUIDOrTANSetException import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException +import de.rki.coronawarnapp.exception.TestAlreadyPairedException +import de.rki.coronawarnapp.exception.TestPairingInvalidException import de.rki.coronawarnapp.http.WebRequestBuilder import de.rki.coronawarnapp.service.submission.SubmissionConstants.QR_CODE_KEY_TYPE +import de.rki.coronawarnapp.service.submission.SubmissionConstants.SERVER_ERROR_CODE_400 import de.rki.coronawarnapp.service.submission.SubmissionConstants.TELE_TAN_KEY_TYPE import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction import de.rki.coronawarnapp.util.formatter.TestResult +import retrofit2.HttpException object SubmissionService { suspend fun asyncRegisterDevice() { - val testGUID = LocalData.testGUID() - val testTAN = LocalData.teletan() - - when { - testGUID != null -> asyncRegisterDeviceViaGUID(testGUID) - testTAN != null -> asyncRegisterDeviceViaTAN(testTAN) - else -> throw NoGUIDOrTANSetException() + try { + val testGUID = LocalData.testGUID() + val testTAN = LocalData.teletan() + + when { + testGUID != null -> asyncRegisterDeviceViaGUID(testGUID) + testTAN != null -> asyncRegisterDeviceViaTAN(testTAN) + else -> throw NoGUIDOrTANSetException() + } + LocalData.devicePairingSuccessfulTimestamp(System.currentTimeMillis()) + } catch (err: HttpException) { + if (err.code() == SERVER_ERROR_CODE_400) { + throw TestAlreadyPairedException( + "the test was already paired to a different device", + err + ) + } + throw err } - LocalData.devicePairingSuccessfulTimestamp(System.currentTimeMillis()) } private suspend fun asyncRegisterDeviceViaGUID(guid: String) { @@ -45,8 +59,17 @@ object SubmissionService { } suspend fun asyncRequestAuthCode(registrationToken: String): String { - val authCode = WebRequestBuilder.asyncGetTan(registrationToken) - return authCode + try { + return WebRequestBuilder.asyncGetTan(registrationToken) + } catch (err: HttpException) { + if (err.code() == SERVER_ERROR_CODE_400) { + throw TestPairingInvalidException( + "the test paring to the device is invalid", + err + ) + } + throw err + } } suspend fun asyncSubmitExposureKeys() { @@ -88,8 +111,8 @@ object SubmissionService { LocalData.devicePairingSuccessfulTimestamp(0L) } - private fun deleteAuthCode() { - LocalData.authCode(null) + fun submissionSuccessful() { + LocalData.numberOfSuccessfulSubmissions(1) } private fun deleteTeleTAN() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt index 734d06ac111..c6c6bba9de2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt @@ -4,6 +4,7 @@ import android.content.SharedPreferences import androidx.core.content.edit import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.risk.RiskLevel import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance import java.util.Date @@ -45,6 +46,33 @@ object LocalData { ) } + /** + * Gets the time when the user has completed the onboarding + * from the EncryptedSharedPrefs + * + * @return + */ + fun onboardingCompletedTimestamp(): Long? { + val timestamp = getSharedPreferenceInstance().getLong( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_onboarding_completed_timestamp), 0L + ) + + if (timestamp == 0L) return null + return timestamp + } + + /** + * Sets the time when the user has completed the onboarding + * from the EncryptedSharedPrefs + * @param value + */ + fun onboardingCompletedTimestamp(value: Long) = getSharedPreferenceInstance().edit(true) { + putLong( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_onboarding_completed_timestamp), value + ) + } /**************************************************** * TRACING DATA ****************************************************/ @@ -146,6 +174,78 @@ object LocalData { } } + /**************************************************** + * RISK LEVEL + ****************************************************/ + + /** + * Gets the last calculated risk level + * from the EncryptedSharedPrefs + * + * @see RiskLevelRepository + * + * @return + */ + fun lastCalculatedRiskLevel(): RiskLevel { + val rawRiskLevel = getSharedPreferenceInstance().getInt( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_risk_level_score), + RiskLevel.UNDETERMINED.raw + ) + return RiskLevel.forValue(rawRiskLevel) + } + + /** + * Sets the last calculated risk level + * from the EncryptedSharedPrefs + * + * @see RiskLevelRepository + * + * @param rawRiskLevel + */ + fun lastCalculatedRiskLevel(rawRiskLevel: Int) = + getSharedPreferenceInstance().edit(true) { + putInt( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_risk_level_score), + rawRiskLevel + ) + } + + /** + * Gets the last successfully calculated risk level + * from the EncryptedSharedPrefs + * + * @see RiskLevelRepository + * + * @return + */ + fun lastSuccessfullyCalculatedRiskLevel(): RiskLevel { + val rawRiskLevel = getSharedPreferenceInstance().getInt( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_risk_level_score_successful), + RiskLevel.UNDETERMINED.raw + ) + return RiskLevel.forValue(rawRiskLevel) + } + + /** + * Sets the last calculated risk level + * from the EncryptedSharedPrefs + * + * @see RiskLevelRepository + * + * @param rawRiskLevel + */ + fun lastSuccessfullyCalculatedRiskLevel(rawRiskLevel: Int) = + getSharedPreferenceInstance().edit(true) { + putInt( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_risk_level_score_successful), + rawRiskLevel + ) + } + /**************************************************** * SERVER FETCH DATA ****************************************************/ @@ -469,18 +569,20 @@ object LocalData { CoronaWarnApplication.getAppContext().getString(R.string.preference_teletan), null ) + fun last3HoursMode(value: Boolean) = getSharedPreferenceInstance().edit(true) { + putBoolean( + CoronaWarnApplication.getAppContext().getString(R.string.preference_last_three_hours_from_server), + value + ) + } + + fun last3HoursMode(): Boolean = getSharedPreferenceInstance().getBoolean( + CoronaWarnApplication.getAppContext().getString(R.string.preference_last_three_hours_from_server), false + ) + /**************************************************** * ENCRYPTED SHARED PREFERENCES HANDLING ****************************************************/ fun getSharedPreferenceInstance(): SharedPreferences = globalEncryptedSharedPreferencesInstance - - fun getBackgroundWorkRelatedPreferences() = listOf( - CoronaWarnApplication.getAppContext().getString(R.string.preference_background_job_allowed), - CoronaWarnApplication.getAppContext().getString(R.string.preference_mobile_data_allowed) - ) - - fun getLastFetchDatePreference() = - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_diagnosis_keys_fetch) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt index d2ecaaa45a6..61f9627f2b6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt @@ -1,33 +1,66 @@ package de.rki.coronawarnapp.storage import androidx.lifecycle.MutableLiveData -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.R import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.risk.RiskLevel.UNDETERMINED import de.rki.coronawarnapp.risk.RiskLevelConstants object RiskLevelRepository { + /** + * LiveData variable that can be consumed in a ViewModel to observe RiskLevel changes + */ val riskLevelScore = MutableLiveData(RiskLevelConstants.UNKNOWN_RISK_INITIAL) - fun setRiskLevelScore(score: RiskLevel) { - val rawRiskLevel = score.raw + /** + * Set the new calculated [RiskLevel] + * Calculation happens in the [de.rki.coronawarnapp.transaction.RiskLevelTransaction] + * + * @see de.rki.coronawarnapp.transaction.RiskLevelTransaction + * @see de.rki.coronawarnapp.risk.RiskLevelCalculation + * + * @param riskLevel + */ + fun setRiskLevelScore(riskLevel: RiskLevel) { + val rawRiskLevel = riskLevel.raw riskLevelScore.postValue(rawRiskLevel) - LocalData.getSharedPreferenceInstance() - .edit() - .putInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score), - rawRiskLevel - ).apply() + + setLastCalculatedScore(rawRiskLevel) + setLastSuccessfullyCalculatedScore(riskLevel) } - fun getLastCalculatedScore(): RiskLevel { - val riskLevelScoreRaw = LocalData.getSharedPreferenceInstance().getInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score), UNDETERMINED.raw - ) - return RiskLevel.forValue(riskLevelScoreRaw) + /** + * Get the last calculated RiskLevel + * + * @return + */ + fun getLastCalculatedScore(): RiskLevel = LocalData.lastCalculatedRiskLevel() + + /** + * Set the last calculated RiskLevel + * + * @param rawRiskLevel + */ + private fun setLastCalculatedScore(rawRiskLevel: Int) = + LocalData.lastCalculatedRiskLevel(rawRiskLevel) + + /** + * Get the last successfully calculated [RiskLevel] + * + * @see RiskLevel + * + * @return + */ + fun getLastSuccessfullyCalculatedScore(): RiskLevel = + LocalData.lastSuccessfullyCalculatedRiskLevel() + + /** + * Set the last successfully calculated [RiskLevel] + * + * @param riskLevel + */ + private fun setLastSuccessfullyCalculatedScore(riskLevel: RiskLevel) { + if (!RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(riskLevel)) { + LocalData.lastSuccessfullyCalculatedRiskLevel(riskLevel.raw) + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt index 918f0ad3a4b..308385ca1c5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt @@ -24,7 +24,6 @@ object SettingsRepository { val isManualKeyRetrievalEnabled = MutableLiveData(true) val isConnectionEnabled = MutableLiveData(true) val isBluetoothEnabled = MutableLiveData(true) - val isMobileDataEnabled = MutableLiveData(true) val isBackgroundJobEnabled = MutableLiveData(true) // TODO should go to a formatter @@ -97,40 +96,11 @@ object SettingsRepository { } /** - * Toggle mobile data in shared preferences and refresh it afterwards. - * - * @see LocalData - */ - fun toggleMobileDataEnabled() { - LocalData.toggleMobileDataEnabled() - refreshMobileDataEnabled() - } - - /** - * Refresh mobile data with the current shared preferences state. - * - * @see LocalData - */ - fun refreshMobileDataEnabled() { - isMobileDataEnabled.value = LocalData.isMobileDataEnabled() - } - - /** - * Toggle background job in shared preferences and refresh it afterwards. - * - * @see LocalData - */ - fun toggleBackgroundJobEnabled() { - LocalData.toggleBackgroundJobEnabled() - refreshBackgroundJobEnabled() - } - - /** - * Refresh background job with the current shared preferences state. + * Refresh global bluetooth state to point out that tracing isn't working * - * @see LocalData + * @see ConnectivityHelper */ - fun refreshBackgroundJobEnabled() { - isBackgroundJobEnabled.value = LocalData.isBackgroundJobEnabled() + fun updateBackgroundJobEnabled(value: Boolean) { + isBackgroundJobEnabled.postValue(value) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt index 49e9ff6f798..0382e75115f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt @@ -230,17 +230,23 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { /** * Executes the API_SUBMISSION Transaction State + * + * We currently use Batch Size 1 and thus submit multiple times to the API. + * This means that instead of directly submitting all files at once, we have to split up + * our file list as this equals a different batch for Google every time. */ private suspend fun executeAPISubmission( token: String, exportFiles: Collection, exposureConfiguration: ExposureConfiguration? ) = executeState(API_SUBMISSION) { - InternalExposureNotificationClient.asyncProvideDiagnosisKeys( - exportFiles, - exposureConfiguration, - token - ) + exportFiles.forEach { batch -> + InternalExposureNotificationClient.asyncProvideDiagnosisKeys( + listOf(batch), + exposureConfiguration, + token + ) + } Log.d(TAG, "Diagnosis Keys provided successfully, Token: $token") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt index 32e157f3771..df32874cc60 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt @@ -33,7 +33,6 @@ import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactio import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.RETRIEVE_APPLICATION_CONFIG import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.RETRIEVE_EXPOSURE_SUMMARY import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.UPDATE_RISK_LEVEL -import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -284,7 +283,7 @@ object RiskLevelTransaction : Transaction() { /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */ - if (timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToDays() > + if (timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() > TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold() ) { return@executeState UNKNOWN_RISK_OUTDATED_RESULTS.also { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt index d7cc02559b5..fb9b2cb2652 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt @@ -7,6 +7,7 @@ import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDia import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDiagnosisKeysTransactionState.RETRIEVE_TAN import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDiagnosisKeysTransactionState.RETRIEVE_TEMPORARY_EXPOSURE_KEY_HISTORY import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDiagnosisKeysTransactionState.SUBMIT_KEYS +import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction.SubmitDiagnosisKeysTransactionState.STORE_SUCCESS import de.rki.coronawarnapp.util.ProtoFormatConverterExtensions.limitKeyCount import de.rki.coronawarnapp.util.ProtoFormatConverterExtensions.transformKeyHistoryToExternalFormat @@ -43,6 +44,7 @@ object SubmitDiagnosisKeysTransaction : Transaction() { RETRIEVE_TAN, RETRIEVE_TEMPORARY_EXPOSURE_KEY_HISTORY, SUBMIT_KEYS, + STORE_SUCCESS, CLOSE } @@ -68,6 +70,12 @@ object SubmitDiagnosisKeysTransaction : Transaction() { executeState(SUBMIT_KEYS) { DiagnosisKeyService.asyncSubmitKeys(authCode, temporaryExposureKeyList) } + /**************************************************** + * STORE SUCCESS + ****************************************************/ + executeState(STORE_SUCCESS) { + SubmissionService.submissionSuccessful() + } /**************************************************** * CLOSE TRANSACTION ****************************************************/ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityErrorReporting.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityErrorReporting.kt new file mode 100644 index 00000000000..7a3db2f4924 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityErrorReporting.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.ui + +import androidx.appcompat.app.AppCompatActivity +import de.rki.coronawarnapp.exception.handler.GlobalExceptionHandlerConstants +import de.rki.coronawarnapp.exception.reportGeneric + +/** + * If the app crashed in the last instance and was restarted, the stacktrace is retrieved + * from the intent and displayed in a dialog report + * + * @see de.rki.coronawarnapp.exception.handler.GlobalExceptionHandler + */ +fun AppCompatActivity.showDialogWithStacktraceIfPreviouslyCrashed() { + val appCrashedAndWasRestarted = + intent.getBooleanExtra(GlobalExceptionHandlerConstants.APP_CRASHED, false) + if (appCrashedAndWasRestarted) { + val stackTrade = intent.getStringExtra(GlobalExceptionHandlerConstants.STACK_TRACE) + if (!stackTrade.isNullOrEmpty()) { + reportGeneric(stackTrade) + } + } +} 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 2f6612a78c3..ff3060508f8 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 @@ -5,6 +5,7 @@ import android.net.Uri import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import de.rki.coronawarnapp.exception.handler.GlobalExceptionHandlerConstants import de.rki.coronawarnapp.http.DynamicURLs import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity @@ -17,7 +18,6 @@ class LauncherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - retrieveCustomURLsFromSchema(intent.data) if (LocalData.isOnboarded()) { @@ -56,12 +56,33 @@ class LauncherActivity : AppCompatActivity() { } private fun startOnboardingActivity() { - startActivity(Intent(this, OnboardingActivity::class.java)) + val onboardingActivity = Intent(this, OnboardingActivity::class.java) + mapIntentExtras(onboardingActivity) + startActivity(onboardingActivity) finish() } private fun startMainActivity() { - startActivity(Intent(this, MainActivity::class.java)) + val mainActivityIntent = Intent(this, MainActivity::class.java) + mapIntentExtras(mainActivityIntent) + startActivity(mainActivityIntent) finish() } + + /** + * Maps the intentExtras for global exception handling to the next activity that is + * started + * + * @param intentForNextActivity + */ + private fun mapIntentExtras(intentForNextActivity: Intent) { + intentForNextActivity.putExtra( + GlobalExceptionHandlerConstants.APP_CRASHED, + intent.getBooleanExtra(GlobalExceptionHandlerConstants.APP_CRASHED, false) + ) + intentForNextActivity.putExtra( + GlobalExceptionHandlerConstants.STACK_TRACE, + intent.getStringExtra(GlobalExceptionHandlerConstants.STACK_TRACE) + ) + } } 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 7107692858b..95a4182275b 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 @@ -16,18 +16,25 @@ class InformationAboutFragment : BaseFragment() { private val TAG: String? = InformationAboutFragment::class.simpleName } - private lateinit var binding: FragmentInformationAboutBinding + private var _binding: FragmentInformationAboutBinding? = null + private val binding: FragmentInformationAboutBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationAboutBinding.inflate(inflater) + _binding = FragmentInformationAboutBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/information/InformationContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt index 3da4c84a61b..88f37c5fd8b 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 @@ -18,18 +18,25 @@ class InformationContactFragment : BaseFragment() { private val TAG: String? = InformationContactFragment::class.simpleName } - private lateinit var binding: FragmentInformationContactBinding + private var _binding: FragmentInformationContactBinding? = null + private val binding: FragmentInformationContactBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationContactBinding.inflate(inflater) + _binding = FragmentInformationContactBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/information/InformationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt index 76866d570f1..80e7e541cd5 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 @@ -18,18 +18,25 @@ class InformationFragment : BaseFragment() { private val TAG: String? = InformationFragment::class.simpleName } - private lateinit var binding: FragmentInformationBinding + private var _binding: FragmentInformationBinding? = null + private val binding: FragmentInformationBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationBinding.inflate(inflater) + _binding = FragmentInformationBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/information/InformationLegalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt index 2d32a639d14..16abadd8c53 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 @@ -16,18 +16,25 @@ class InformationLegalFragment : BaseFragment() { private val TAG: String? = InformationLegalFragment::class.simpleName } - private lateinit var binding: FragmentInformationLegalBinding + private var _binding: FragmentInformationLegalBinding? = null + private val binding: FragmentInformationLegalBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationLegalBinding.inflate(inflater) + _binding = FragmentInformationLegalBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/information/InformationPrivacyFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt index 7b3cc61e44c..b3ab39e4cc4 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 @@ -16,18 +16,25 @@ class InformationPrivacyFragment : BaseFragment() { private val TAG: String? = InformationPrivacyFragment::class.simpleName } - private lateinit var binding: FragmentInformationPrivacyBinding + private var _binding: FragmentInformationPrivacyBinding? = null + private val binding: FragmentInformationPrivacyBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationPrivacyBinding.inflate(inflater) + _binding = FragmentInformationPrivacyBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/information/InformationTechnicalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt index a4e295f7e5e..9ce43c29ff7 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 @@ -16,18 +16,25 @@ class InformationTechnicalFragment : BaseFragment() { private val TAG: String? = InformationTechnicalFragment::class.simpleName } - private lateinit var binding: FragmentInformationTechnicalBinding + private var _binding: FragmentInformationTechnicalBinding? = null + private val binding: FragmentInformationTechnicalBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationTechnicalBinding.inflate(inflater) + _binding = FragmentInformationTechnicalBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/information/InformationTermsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt index 486172f2a36..8189f124017 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 @@ -16,18 +16,25 @@ class InformationTermsFragment : BaseFragment() { private val TAG: String? = InformationTermsFragment::class.simpleName } - private lateinit var binding: FragmentInformationTermsBinding + private var _binding: FragmentInformationTermsBinding? = null + private val binding: FragmentInformationTermsBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentInformationTermsBinding.inflate(inflater) + _binding = FragmentInformationTermsBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index f6643d11320..1d03c732ac9 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 @@ -1,15 +1,14 @@ package de.rki.coronawarnapp.ui.main import android.content.Intent -import android.content.IntentFilter import android.os.Bundle +import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProviders -import androidx.localbroadcastmanager.content.LocalBroadcastManager import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.exception.ErrorReportReceiver +import de.rki.coronawarnapp.ui.showDialogWithStacktraceIfPreviouslyCrashed import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.worker.BackgroundWorkScheduler @@ -32,8 +31,6 @@ class MainActivity : AppCompatActivity() { private lateinit var settingsViewModel: SettingsViewModel - private val errorReceiver = ErrorReportReceiver(this) - /** * Register connection callback. */ @@ -59,10 +56,6 @@ class MainActivity : AppCompatActivity() { } } - init { - scheduleWork() - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -70,13 +63,16 @@ class MainActivity : AppCompatActivity() { } /** - * Register network and bluetooth callback. + * Register network, bluetooth and data saver callback. */ override fun onResume() { super.onResume() - LocalBroadcastManager.getInstance(this).registerReceiver(errorReceiver, IntentFilter("error-report")) ConnectivityHelper.registerNetworkStatusCallback(this, callbackNetwork) ConnectivityHelper.registerBluetoothStatusCallback(this, callbackBluetooth) + Log.d(TAG, "Background work is available: ${!ConnectivityHelper.isDataSaverEnabled(this)}") + settingsViewModel.updateBackgroundJobEnabled(!ConnectivityHelper.isDataSaverEnabled(this)) + scheduleWork() + showDialogWithStacktraceIfPreviouslyCrashed() } /** @@ -86,8 +82,6 @@ class MainActivity : AppCompatActivity() { super.onPause() ConnectivityHelper.unregisterNetworkStatusCallback(this, callbackNetwork) ConnectivityHelper.unregisterBluetoothStatusCallback(this, callbackBluetooth) - // Unregister since the activity is about to be closed. - LocalBroadcastManager.getInstance(this).unregisterReceiver(errorReceiver) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt index 6f9355b5220..3634469bef8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt @@ -38,14 +38,15 @@ class MainFragment : BaseFragment() { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() private val submissionViewModel: SubmissionViewModel by activityViewModels() - private lateinit var binding: FragmentMainBinding + private var _binding: FragmentMainBinding? = null + private val binding: FragmentMainBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentMainBinding.inflate(inflater) + _binding = FragmentMainBinding.inflate(inflater) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.submissionViewModel = submissionViewModel @@ -53,6 +54,11 @@ class MainFragment : BaseFragment() { return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() @@ -66,7 +72,6 @@ class MainFragment : BaseFragment() { tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate() tracingViewModel.refreshIsTracingEnabled() tracingViewModel.refreshActiveTracingDaysInRetentionPeriod() - settingsViewModel.refreshBackgroundJobEnabled() TimerHelper.checkManualKeyRetrievalTimer() submissionViewModel.refreshDeviceUIState() } 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 03d4f392972..40502e58607 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 @@ -20,17 +20,23 @@ class MainOverviewFragment : BaseFragment() { private val TAG: String? = MainOverviewFragment::class.simpleName } - private lateinit var binding: FragmentMainOverviewBinding + private var _binding: FragmentMainOverviewBinding? = null + private val binding: FragmentMainOverviewBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentMainOverviewBinding.inflate(inflater) + _binding = FragmentMainOverviewBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/main/MainShareFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt index e6b42e0eab6..8ba1bb2e6fc 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 @@ -23,19 +23,25 @@ class MainShareFragment : BaseFragment() { } private val tracingViewModel: TracingViewModel by activityViewModels() - private lateinit var binding: FragmentMainShareBinding + private var _binding: FragmentMainShareBinding? = null + private val binding: FragmentMainShareBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentMainShareBinding.inflate(inflater) + _binding = FragmentMainShareBinding.inflate(inflater) binding.tracingViewModel = tracingViewModel binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/onboarding/OnboardingActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt index 203301626e0..bee511d98e3 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 @@ -9,6 +9,7 @@ import androidx.lifecycle.LifecycleObserver import de.rki.coronawarnapp.R import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity +import de.rki.coronawarnapp.ui.showDialogWithStacktraceIfPreviouslyCrashed /** * This activity holds all the onboarding fragments and isn't used after a successful onboarding flow. @@ -40,8 +41,14 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver { ) } + override fun onResume() { + super.onResume() + showDialogWithStacktraceIfPreviouslyCrashed() + } + fun completeOnboarding() { LocalData.isOnboarded(true) + LocalData.onboardingCompletedTimestamp(System.currentTimeMillis()) startActivity(Intent(this, MainActivity::class.java)) finish() } 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 da9b2965183..bad65043b8e 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 @@ -15,17 +15,23 @@ class OnboardingFragment : BaseFragment() { private val TAG: String? = OnboardingFragment::class.simpleName } - private lateinit var binding: FragmentOnboardingBinding + private var _binding: FragmentOnboardingBinding? = null + private val binding: FragmentOnboardingBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentOnboardingBinding.inflate(inflater) + _binding = FragmentOnboardingBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.onboardingButtonNext.setOnClickListener { 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 11f6fb9ba9e..7b9c9879532 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 @@ -20,17 +20,23 @@ class OnboardingNotificationsFragment : BaseFragment() { private val TAG: String? = OnboardingNotificationsFragment::class.simpleName } - private lateinit var binding: FragmentOnboardingNotificationsBinding + private var _binding: FragmentOnboardingNotificationsBinding? = null + private val binding: FragmentOnboardingNotificationsBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentOnboardingNotificationsBinding.inflate(inflater) + _binding = FragmentOnboardingNotificationsBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/onboarding/OnboardingPrivacyFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingPrivacyFragment.kt index 941ece49508..21bacc7920e 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 @@ -15,17 +15,23 @@ class OnboardingPrivacyFragment : BaseFragment() { private val TAG: String? = OnboardingPrivacyFragment::class.simpleName } - private lateinit var binding: FragmentOnboardingPrivacyBinding + private var _binding: FragmentOnboardingPrivacyBinding? = null + private val binding: FragmentOnboardingPrivacyBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentOnboardingPrivacyBinding.inflate(inflater) + _binding = FragmentOnboardingPrivacyBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/onboarding/OnboardingTestFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTestFragment.kt index 248c12d9142..494b6fbd7c8 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 @@ -15,17 +15,23 @@ class OnboardingTestFragment : BaseFragment() { private val TAG: String? = OnboardingTestFragment::class.simpleName } - private lateinit var binding: FragmentOnboardingTestBinding + private var _binding: FragmentOnboardingTestBinding? = null + private val binding: FragmentOnboardingTestBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentOnboardingTestBinding.inflate(inflater) + _binding = FragmentOnboardingTestBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/onboarding/OnboardingTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt index 15cc2dce96a..023b07e91f9 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 @@ -26,7 +26,8 @@ class OnboardingTracingFragment : BaseFragment(), } private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper - private lateinit var binding: FragmentOnboardingTracingBinding + private var _binding: FragmentOnboardingTracingBinding? = null + private val binding: FragmentOnboardingTracingBinding get() = _binding!! override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { internalExposureNotificationPermissionHelper.onResolutionComplete( @@ -46,10 +47,15 @@ class OnboardingTracingFragment : BaseFragment(), container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentOnboardingTracingBinding.inflate(inflater) + _binding = FragmentOnboardingTracingBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/riskdetails/RiskDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt index 7e0a5d01ef1..893a95287d2 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 @@ -26,21 +26,28 @@ class RiskDetailsFragment : BaseFragment() { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() - private lateinit var binding: FragmentRiskDetailsBinding + private var _binding: FragmentRiskDetailsBinding? = null + private val binding: FragmentRiskDetailsBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentRiskDetailsBinding.inflate(inflater) + _binding = FragmentRiskDetailsBinding.inflate(inflater) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) setButtonOnClickListeners() } @@ -50,7 +57,6 @@ class RiskDetailsFragment : BaseFragment() { tracingViewModel.refreshRiskLevel() tracingViewModel.refreshExposureSummary() tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate() - settingsViewModel.refreshBackgroundJobEnabled() TimerHelper.checkManualKeyRetrievalTimer() } 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 ed39f371606..f7aab9c197b 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 @@ -25,20 +25,26 @@ class SettingsFragment : BaseFragment() { private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() - private lateinit var binding: FragmentSettingsBinding + private var _binding: FragmentSettingsBinding? = null + private val binding: FragmentSettingsBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSettingsBinding.inflate(inflater) + _binding = FragmentSettingsBinding.inflate(inflater) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() @@ -51,8 +57,6 @@ class SettingsFragment : BaseFragment() { settingsViewModel.refreshNotificationsEnabled(requireContext()) settingsViewModel.refreshNotificationsRiskEnabled() settingsViewModel.refreshNotificationsTestEnabled() - settingsViewModel.refreshMobileDataEnabled() - settingsViewModel.refreshBackgroundJobEnabled() } private fun 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 695362b30c0..2344f700960 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 @@ -26,19 +26,25 @@ class SettingsNotificationFragment : Fragment() { } private val settingsViewModel: SettingsViewModel by activityViewModels() - private lateinit var binding: FragmentSettingsNotificationsBinding + private var _binding: FragmentSettingsNotificationsBinding? = null + private val binding: FragmentSettingsNotificationsBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSettingsNotificationsBinding.inflate(inflater) + _binding = FragmentSettingsNotificationsBinding.inflate(inflater) binding.settingsViewModel = settingsViewModel binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/settings/SettingsResetFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt index 0606f781908..6fb673f152f 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 @@ -30,17 +30,23 @@ class SettingsResetFragment : BaseFragment() { private val TAG: String? = SettingsResetFragment::class.simpleName } - private lateinit var binding: FragmentSettingsResetBinding + private var _binding: FragmentSettingsResetBinding? = null + private val binding: FragmentSettingsResetBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSettingsResetBinding.inflate(inflater) + _binding = FragmentSettingsResetBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.settingsResetButtonDelete.setOnClickListener { 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 219dfa2cd4b..382d34f0557 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 @@ -39,7 +39,8 @@ class SettingsTracingFragment : BaseFragment(), private val tracingViewModel: TracingViewModel by activityViewModels() private val settingsViewModel: SettingsViewModel by activityViewModels() - private lateinit var binding: FragmentSettingsTracingBinding + private var _binding: FragmentSettingsTracingBinding? = null + private val binding: FragmentSettingsTracingBinding get() = _binding!! private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper @@ -48,14 +49,20 @@ class SettingsTracingFragment : BaseFragment(), container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSettingsTracingBinding.inflate(inflater) + _binding = FragmentSettingsTracingBinding.inflate(inflater) binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() } @@ -74,8 +81,7 @@ class SettingsTracingFragment : BaseFragment(), override fun onStartPermissionGranted() { tracingViewModel.refreshIsTracingEnabled() - // TODO - BackgroundWorkScheduler.checkStart() + BackgroundWorkScheduler.startWorkScheduler() Toast.makeText(requireContext(), "Tracing started successfully", Toast.LENGTH_SHORT).show() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt index 14607969a60..0d1693898f5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt @@ -15,7 +15,8 @@ import de.rki.coronawarnapp.util.CallHelper */ class SubmissionContactFragment : BaseFragment() { - private lateinit var binding: FragmentSubmissionContactBinding + private var _binding: FragmentSubmissionContactBinding? = null + private val binding: FragmentSubmissionContactBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -23,10 +24,15 @@ class SubmissionContactFragment : BaseFragment() { savedInstanceState: Bundle? ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionContactBinding.inflate(inflater) + _binding = FragmentSubmissionContactBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/SubmissionDispatcherFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt index c7e4bf5b4bf..4178eea3bd4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionDispatcherBinding import de.rki.coronawarnapp.ui.BaseFragment +import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.DialogHelper class SubmissionDispatcherFragment : BaseFragment() { @@ -15,24 +16,33 @@ class SubmissionDispatcherFragment : BaseFragment() { private val TAG: String? = SubmissionDispatcherFragment::class.simpleName } - private lateinit var binding: FragmentSubmissionDispatcherBinding + private var _binding: FragmentSubmissionDispatcherBinding? = null + private val binding: FragmentSubmissionDispatcherBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSubmissionDispatcherBinding.inflate(inflater) + _binding = FragmentSubmissionDispatcherBinding.inflate(inflater) binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() } private fun setButtonOnClickListener() { + binding.submissionDispatcherHeader.headerButtonBack.buttonIcon.setOnClickListener { + (activity as MainActivity).goBack() + } binding.submissionDispatcherQr.dispatcherCard.setOnClickListener { checkForDataPrivacyPermission() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt index af9ab63b466..330c86d79ff 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt @@ -12,7 +12,8 @@ import de.rki.coronawarnapp.ui.BaseFragment */ class SubmissionDoneFragment : BaseFragment() { - private lateinit var binding: FragmentSubmissionDoneBinding + private var _binding: FragmentSubmissionDoneBinding? = null + private val binding: FragmentSubmissionDoneBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -20,22 +21,22 @@ class SubmissionDoneFragment : BaseFragment() { savedInstanceState: Bundle? ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionDoneBinding.inflate(inflater) + _binding = FragmentSubmissionDoneBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() } private fun setButtonOnClickListener() { - binding - .submissionDoneInclude - .submissionDoneHeader - .informationHeader - .headerButtonBack.buttonIcon - .setOnClickListener { + binding.submissionDoneHeader.headerButtonBack.buttonIcon.setOnClickListener { doNavigate( SubmissionDoneFragmentDirections.actionSubmissionDoneFragmentToMainFragment() ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt index 279b084230f..219457cb2e0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt @@ -12,7 +12,8 @@ import de.rki.coronawarnapp.ui.BaseFragment */ class SubmissionIntroFragment : BaseFragment() { - private lateinit var binding: FragmentSubmissionIntroBinding + private var _binding: FragmentSubmissionIntroBinding? = null + private val binding: FragmentSubmissionIntroBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -20,10 +21,15 @@ class SubmissionIntroFragment : BaseFragment() { savedInstanceState: Bundle? ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionIntroBinding.inflate(inflater) + _binding = FragmentSubmissionIntroBinding.inflate(inflater) return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + 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/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt index ad089960cb4..bcee7773ca8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt @@ -5,18 +5,20 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer +import androidx.fragment.app.activityViewModels import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.BarcodeResult import com.journeyapps.barcodescanner.DefaultDecoderFactory import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionQrCodeScanBinding +import de.rki.coronawarnapp.exception.TestAlreadyPairedException import de.rki.coronawarnapp.ui.BaseFragment import de.rki.coronawarnapp.ui.main.MainActivity 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 retrofit2.HttpException /** * A simple [BaseFragment] subclass. @@ -28,15 +30,16 @@ class SubmissionQRCodeScanFragment : BaseFragment() { private val TAG: String? = SubmissionQRCodeScanFragment::class.simpleName } - private val viewModel: SubmissionViewModel by viewModels() - private lateinit var binding: FragmentSubmissionQrCodeScanBinding + private val viewModel: SubmissionViewModel by activityViewModels() + private var _binding: FragmentSubmissionQrCodeScanBinding? = null + private val binding: FragmentSubmissionQrCodeScanBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSubmissionQrCodeScanBinding.inflate(inflater) + _binding = FragmentSubmissionQrCodeScanBinding.inflate(inflater) binding.lifecycleOwner = this return binding.root } @@ -49,6 +52,46 @@ class SubmissionQRCodeScanFragment : BaseFragment() { binding.submissionQrCodeScanPreview.decodeSingle { decodeCallback(it) } } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance { + return when (exception) { + is TestAlreadyPairedException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_test_paired_title, + R.string.submission_error_dialog_web_test_paired_body, + R.string.submission_error_dialog_web_test_paired_button_positive, + null, + true, + ::navigateToDispatchScreen + ) + is HttpException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + getString( + R.string.submission_error_dialog_web_generic_network_error_body, + exception.code() + ), + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToDispatchScreen + ) + else -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + R.string.submission_error_dialog_web_generic_error_body, + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToDispatchScreen + ) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -66,7 +109,7 @@ class SubmissionQRCodeScanFragment : BaseFragment() { DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) binding.submissionQrCodeScanViewfinderView.setCameraPreview(binding.submissionQrCodeScanPreview) - viewModel.scanStatus.observe(viewLifecycleOwner, Observer { + viewModel.scanStatus.observeEvent(viewLifecycleOwner, { if (ScanStatus.SUCCESS == it) { showSuccessfulScanDialog() } @@ -75,6 +118,19 @@ class SubmissionQRCodeScanFragment : BaseFragment() { showInvalidScanDialog() } }) + + viewModel.registrationState.observeEvent(viewLifecycleOwner, { + if (ApiRequestState.SUCCESS == it) { + doNavigate( + SubmissionQRCodeScanFragmentDirections + .actionSubmissionQRCodeScanFragmentToSubmissionResultFragment() + ) + } + }) + + viewModel.registrationError.observeEvent(viewLifecycleOwner, { + DialogHelper.showDialog(buildErrorDialog(it)) + }) } private fun navigateToDispatchScreen() = @@ -92,10 +148,7 @@ class SubmissionQRCodeScanFragment : BaseFragment() { R.string.submission_qr_code_scan_successful_dialog_button_negative, true, { - doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionRegisterDeviceFragment() - ) + viewModel.doDeviceRegistration() }, { viewModel.deleteTestGUID() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt deleted file mode 100644 index 853c16b6e8b..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt +++ /dev/null @@ -1,44 +0,0 @@ -package de.rki.coronawarnapp.ui.submission - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer -import de.rki.coronawarnapp.databinding.FragmentSubmissionRegisterDeviceBinding -import de.rki.coronawarnapp.ui.BaseFragment -import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel - -class SubmissionRegisterDeviceFragment : BaseFragment() { - private val viewModel: SubmissionViewModel by activityViewModels() - private lateinit var binding: FragmentSubmissionRegisterDeviceBinding - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentSubmissionRegisterDeviceBinding.inflate(inflater) - binding.lifecycleOwner = this - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewModel.registrationState.observe(viewLifecycleOwner, Observer { - if (ApiRequestState.SUCCESS == it) { - doNavigate( - SubmissionRegisterDeviceFragmentDirections - .actionSubmissionRegisterDeviceFragmentToSubmissionResultFragment() - ) - } - }) - } - - override fun onResume() { - super.onResume() - viewModel.doDeviceRegistration() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt index 527afa53285..477722597f3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt @@ -5,15 +5,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Observer import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionPositiveOtherWarningBinding +import de.rki.coronawarnapp.exception.SubmissionTanInvalidException +import de.rki.coronawarnapp.exception.TestPairingInvalidException import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.ui.BaseFragment 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 retrofit2.HttpException class SubmissionResultPositiveOtherWarningFragment : BaseFragment(), InternalExposureNotificationPermissionHelper.Callback { @@ -25,7 +28,8 @@ class SubmissionResultPositiveOtherWarningFragment : BaseFragment(), private val submissionViewModel: SubmissionViewModel by activityViewModels() private val tracingViewModel: TracingViewModel by activityViewModels() - private lateinit var binding: FragmentSubmissionPositiveOtherWarningBinding + private var _binding: FragmentSubmissionPositiveOtherWarningBinding? = null + private val binding: FragmentSubmissionPositiveOtherWarningBinding get() = _binding!! private var submissionRequested = false private var submissionFailed = false private lateinit var internalExposureNotificationPermissionHelper: @@ -55,16 +59,69 @@ class SubmissionResultPositiveOtherWarningFragment : BaseFragment(), ): View? { internalExposureNotificationPermissionHelper = InternalExposureNotificationPermissionHelper(this, this) - binding = FragmentSubmissionPositiveOtherWarningBinding.inflate(inflater) + _binding = FragmentSubmissionPositiveOtherWarningBinding.inflate(inflater) binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance { + return when (exception) { + is TestPairingInvalidException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_paring_invalid_title, + R.string.submission_error_dialog_web_paring_invalid_body, + R.string.submission_error_dialog_web_paring_invalid_button_positive, + null, + true, + ::navigateToSubmissionResultFragment + ) + is SubmissionTanInvalidException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_tan_invalid_title, + R.string.submission_error_dialog_web_tan_invalid_body, + R.string.submission_error_dialog_web_tan_invalid_button_positive, + null, + true, + ::navigateToSubmissionResultFragment + ) + is HttpException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + getString( + R.string.submission_error_dialog_web_generic_network_error_body, + exception.code() + ), + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToSubmissionResultFragment + ) + else -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + R.string.submission_error_dialog_web_generic_error_body, + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToSubmissionResultFragment + ) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() - submissionViewModel.submissionState.observe(viewLifecycleOwner, Observer { + submissionViewModel.submissionError.observeEvent(viewLifecycleOwner, { + DialogHelper.showDialog(buildErrorDialog(it)) + }) + + submissionViewModel.submissionState.observeEvent(viewLifecycleOwner, { if (it == ApiRequestState.SUCCESS) { doNavigate( SubmissionResultPositiveOtherWarningFragmentDirections @@ -75,18 +132,20 @@ class SubmissionResultPositiveOtherWarningFragment : BaseFragment(), } private fun setButtonOnClickListener() { - binding.submissionPositiveOtherWarningButton.setOnClickListener { + binding.submissionPositiveOtherWarningButtonNext.setOnClickListener { initiateWarningOthers() } - binding.submissionPositiveOtherWarningHeader - .informationHeader.headerButtonBack.buttonIcon.setOnClickListener { - doNavigate( - SubmissionResultPositiveOtherWarningFragmentDirections - .actionSubmissionResultPositiveOtherWarningFragmentToSubmissionResultFragment() - ) - } + binding.submissionPositiveOtherWarningHeader.headerButtonBack.buttonIcon.setOnClickListener { + navigateToSubmissionResultFragment() + } } + private fun navigateToSubmissionResultFragment() = + doNavigate( + SubmissionResultPositiveOtherWarningFragmentDirections + .actionSubmissionResultPositiveOtherWarningFragmentToSubmissionResultFragment() + ) + private fun initiateWarningOthers() { if (tracingViewModel.isTracingEnabled.value != true) { val tracingRequiredDialog = DialogHelper.DialogInstance( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionSuccessDialogFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionSuccessDialogFragment.kt index de42246623d..5b936c1ffbb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionSuccessDialogFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionSuccessDialogFragment.kt @@ -18,7 +18,8 @@ class SubmissionSuccessDialogFragment : DialogFragment() { private val TAG: String? = SubmissionSuccessDialogFragment::class.simpleName } - private lateinit var binding: FragmentSubmissionDialogBinding + private var _binding: FragmentSubmissionDialogBinding? = null + private val binding: FragmentSubmissionDialogBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -26,12 +27,17 @@ class SubmissionSuccessDialogFragment : DialogFragment() { savedInstanceState: Bundle? ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionDialogBinding.inflate(inflater) + _binding = FragmentSubmissionDialogBinding.inflate(inflater) // Inflate the layout for this fragment return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding.submissionVerificationSuccessButton.setOnClickListener { Log.i(TAG, "button OK clicked") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt index 4f05de4597f..56b4c46080f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt @@ -5,8 +5,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.activityViewModels +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionTanBinding +import de.rki.coronawarnapp.exception.TestAlreadyPairedException import de.rki.coronawarnapp.ui.BaseFragment +import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.observeEvent +import retrofit2.HttpException /** * Fragment for TAN entry @@ -14,7 +20,9 @@ import de.rki.coronawarnapp.ui.BaseFragment class SubmissionTanFragment : BaseFragment() { private val viewModel: SubmissionTanViewModel by activityViewModels() - private lateinit var binding: FragmentSubmissionTanBinding + private val submissionViewModel: SubmissionViewModel by activityViewModels() + private var _binding: FragmentSubmissionTanBinding? = null + private val binding: FragmentSubmissionTanBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -22,24 +30,75 @@ class SubmissionTanFragment : BaseFragment() { savedInstanceState: Bundle? ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionTanBinding.inflate(inflater) + _binding = FragmentSubmissionTanBinding.inflate(inflater) binding.viewmodel = viewModel binding.lifecycleOwner = this return binding.root } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance { + return when (exception) { + is TestAlreadyPairedException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_test_paired_title, + R.string.submission_error_dialog_web_test_paired_body, + R.string.submission_error_dialog_web_test_paired_button_positive, + null, + true, + ::navigateToDispatchScreen + ) + is HttpException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + getString( + R.string.submission_error_dialog_web_generic_network_error_body, + exception.code() + ), + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToDispatchScreen + ) + else -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + R.string.submission_error_dialog_web_generic_error_body, + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToDispatchScreen + ) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.submissionTanInput.listener = { tan -> viewModel.tan.value = tan } binding.submissionTanButtonEnter.setOnClickListener { storeTanAndContinue() } - binding.submissionTanHeader.headerButtonBack.buttonIcon.setOnClickListener { close() } - } + binding.submissionTanHeader.headerButtonBack.buttonIcon.setOnClickListener { navigateToDispatchScreen() } + + submissionViewModel.registrationState.observeEvent(viewLifecycleOwner, { + if (ApiRequestState.SUCCESS == it) { + doNavigate( + SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionResultFragment() + ) + } + }) - private fun close() { - doNavigate(SubmissionTanFragmentDirections.actionSubmissionTanFragmentToMainFragment()) + submissionViewModel.registrationError.observeEvent(viewLifecycleOwner, { + DialogHelper.showDialog(buildErrorDialog(it)) + }) } + private fun navigateToDispatchScreen() = + doNavigate(SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionDispatcherFragment()) + private fun storeTanAndContinue() { // verify input format if (viewModel.isValidTanFormat.value != true) @@ -48,6 +107,6 @@ class SubmissionTanFragment : BaseFragment() { // store locally viewModel.storeTeletan() - doNavigate(SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionRegisterDeviceFragment()) + submissionViewModel.doDeviceRegistration() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt index cdb2f61ca13..917c796df28 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt @@ -11,6 +11,8 @@ import de.rki.coronawarnapp.ui.BaseFragment 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 retrofit2.HttpException /** * A simple [BaseFragment] subclass. @@ -23,7 +25,8 @@ class SubmissionTestResultFragment : BaseFragment() { private val submissionViewModel: SubmissionViewModel by activityViewModels() private val tracingViewModel: TracingViewModel by activityViewModels() - private lateinit var binding: FragmentSubmissionTestResultBinding + private var _binding: FragmentSubmissionTestResultBinding? = null + private val binding: FragmentSubmissionTestResultBinding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, @@ -31,16 +34,54 @@ class SubmissionTestResultFragment : BaseFragment() { savedInstanceState: Bundle? ): View? { // get the binding reference by inflating it with the current layout - binding = FragmentSubmissionTestResultBinding.inflate(inflater) + _binding = FragmentSubmissionTestResultBinding.inflate(inflater) binding.submissionViewModel = submissionViewModel binding.lifecycleOwner = this // Inflate the layout for this fragment return binding.root } + private fun navigateToMainScreen() = + doNavigate(SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment()) + + private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance { + return when (exception) { + is HttpException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + getString( + R.string.submission_error_dialog_web_generic_network_error_body, + exception.code() + ), + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToMainScreen + ) + else -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + R.string.submission_error_dialog_web_generic_error_body, + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateToMainScreen + ) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() + + submissionViewModel.uiStateError.observeEvent(viewLifecycleOwner, { + DialogHelper.showDialog(buildErrorDialog(it)) + }) } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt index ada13e7039f..05809e74ce9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt @@ -22,10 +22,6 @@ class SettingsViewModel : ViewModel() { val isBluetoothEnabled: LiveData = SettingsRepository.isBluetoothEnabled - // Todo bind to os settings, change to general network availability, cannot be set within the app - // Will impact UI if no network connection is found, persistent storing is not necessary - val isMobileDataEnabled: LiveData = SettingsRepository.isMobileDataEnabled - // Todo bind to os settings, care API 23 / API 24 onwards // Will impact UI if background activity is not permitted, persistent storing is not necessary val isBackgroundJobEnabled: LiveData = SettingsRepository.isBackgroundJobEnabled @@ -102,24 +98,11 @@ class SettingsViewModel : ViewModel() { } /** - * Refresh & toggle mobile data enabled - */ - fun refreshMobileDataEnabled() { - SettingsRepository.refreshMobileDataEnabled() - } - - fun toggleMobileDataEnabled() { - SettingsRepository.toggleMobileDataEnabled() - } - - /** - * Refresh & toggle background job enabled + * Update background job enabled + * + * @param value */ - fun refreshBackgroundJobEnabled() { - SettingsRepository.refreshBackgroundJobEnabled() - } - - fun toggleBackgroundJobEnabled() { - SettingsRepository.toggleBackgroundJobEnabled() + fun updateBackgroundJobEnabled(value: Boolean) { + SettingsRepository.updateBackgroundJobEnabled(value) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt index de2bb9f5066..39431e54f09 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt @@ -4,27 +4,38 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.Event import kotlinx.coroutines.launch import java.util.Date class SubmissionViewModel : ViewModel() { - private val _scanStatus = MutableLiveData(ScanStatus.STARTED) - private val _registrationState = MutableLiveData(ApiRequestState.IDLE) + private val _scanStatus = MutableLiveData(Event(ScanStatus.STARTED)) + + private val _registrationState = MutableLiveData(Event(ApiRequestState.IDLE)) + private val _registrationError = MutableLiveData>(null) + private val _uiStateState = MutableLiveData(ApiRequestState.IDLE) - private val _submissionState = MutableLiveData(ApiRequestState.IDLE) + private val _uiStateError = MutableLiveData>(null) + + private val _submissionState = MutableLiveData(Event(ApiRequestState.IDLE)) + private val _submissionError = MutableLiveData>(null) + + val scanStatus: LiveData> = _scanStatus + + val registrationState: LiveData> = _registrationState + val registrationError: LiveData> = _registrationError - val scanStatus: LiveData = _scanStatus - val registrationState: LiveData = _registrationState val uiStateState: LiveData = _uiStateState - val submissionState: LiveData = _submissionState + val uiStateError: LiveData> = _uiStateError + + val submissionState: LiveData> = _submissionState + val submissionError: LiveData> = _submissionError val deviceRegistered get() = LocalData.registrationToken() != null @@ -34,21 +45,33 @@ class SubmissionViewModel : ViewModel() { SubmissionRepository.deviceUIState fun submitDiagnosisKeys() = - executeRequestWithState(SubmissionService::asyncSubmitExposureKeys, _submissionState) + executeRequestWithStateForEvent( + SubmissionService::asyncSubmitExposureKeys, + _submissionState, + _submissionError + ) fun doDeviceRegistration() = - executeRequestWithState(SubmissionService::asyncRegisterDevice, _registrationState) + executeRequestWithStateForEvent( + SubmissionService::asyncRegisterDevice, + _registrationState, + _registrationError + ) fun refreshDeviceUIState() = - executeRequestWithState(SubmissionRepository::refreshUIState, _uiStateState) + executeRequestWithState( + SubmissionRepository::refreshUIState, + _uiStateState, + _uiStateError + ) fun validateAndStoreTestGUID(scanResult: String) { if (SubmissionService.containsValidGUID(scanResult)) { val guid = SubmissionService.extractGUID(scanResult) SubmissionService.storeTestGUID(guid) - _scanStatus.value = ScanStatus.SUCCESS + _scanStatus.value = Event(ScanStatus.SUCCESS) } else { - _scanStatus.value = ScanStatus.INVALID + _scanStatus.value = Event(ScanStatus.INVALID) } } @@ -65,7 +88,8 @@ class SubmissionViewModel : ViewModel() { private fun executeRequestWithState( apiRequest: suspend () -> Unit, - state: MutableLiveData + state: MutableLiveData, + exceptionLiveData: MutableLiveData>? = null ) { state.value = ApiRequestState.STARTED viewModelScope.launch { @@ -73,8 +97,25 @@ class SubmissionViewModel : ViewModel() { apiRequest() state.value = ApiRequestState.SUCCESS } catch (err: Exception) { + exceptionLiveData?.value = Event(err) state.value = ApiRequestState.FAILED - err.report(ExceptionCategory.INTERNAL) + } + } + } + + private fun executeRequestWithStateForEvent( + apiRequest: suspend () -> Unit, + state: MutableLiveData>, + exceptionLiveData: MutableLiveData>? = null + ) { + state.value = Event(ApiRequestState.STARTED) + viewModelScope.launch { + try { + apiRequest() + state.value = Event(ApiRequestState.SUCCESS) + } catch (err: Exception) { + exceptionLiveData?.value = Event(err) + state.value = Event(ApiRequestState.FAILED) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt index c7ef6e95de1..9fe260c58c7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt @@ -23,10 +23,10 @@ import android.util.Log import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.http.WebRequestBuilder import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants +import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.keycache.KeyCacheEntity import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository.DateEntryType.DAY -import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository.DateEntryType.HOUR import de.rki.coronawarnapp.util.CachedKeyFileHolder.asyncFetchFiles import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat import kotlinx.coroutines.Deferred @@ -35,6 +35,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import java.io.File +import java.lang.IllegalStateException import java.util.Date import java.util.UUID @@ -65,40 +66,48 @@ object CachedKeyFileHolder { * @return list of all files from both the cache and the diff query */ suspend fun asyncFetchFiles(currentDate: Date): List = withContext(Dispatchers.IO) { - keyCache.deleteOutdatedEntries() - // queries will be executed after the "query plan" was set - val deferredQueries: MutableCollection> = mutableListOf() val serverDates = getDatesFromServer() - val missingDays = getMissingDaysFromDiff(serverDates) - if (missingDays.isNotEmpty()) { - // we have a date difference - deferredQueries.addAll( - missingDays - .map { getURLForDay(it) } - .map { url -> async { url.createDayEntryForUrl() } } - ) - // if we have a date difference we need to refetch the current hours - keyCache.clearHours() - } - val currentDateServerFormat = currentDate.toServerFormat() - // just fetch the hours if the date is available - if (serverDates.contains(currentDateServerFormat)) { - // we have an hour difference - deferredQueries.addAll( - getMissingHoursFromDiff(currentDate) + // TODO remove last3HourFetch before Release + if (isLast3HourFetchEnabled()) { + Log.v(TAG, "Last 3 Hours will be Fetched. Only use for Debugging!") + val currentDateServerFormat = currentDate.toServerFormat() + // just fetch the hours if the date is available + if (serverDates.contains(currentDateServerFormat)) { + return@withContext getLast3Hours(currentDate) .map { getURLForHour(currentDate.toServerFormat(), it) } - .map { url -> async { url.createHourEntryForUrl() } } - ) - } - // execute the query plan - try { - deferredQueries.awaitAll() - } catch (e: Exception) { - // For an error we clear the cache to try again - keyCache.clear() + .map { url -> async { + return@async WebRequestBuilder.asyncGetKeyFilesFromServer(url) + } }.awaitAll() + } else { + throw IllegalStateException( + "you cannot use the last 3 hour mode if the date index " + + "does not contain any data for today" + ) + } + } else { + // queries will be executed after the "query plan" was set + val deferredQueries: MutableCollection> = mutableListOf() + keyCache.deleteOutdatedEntries() + val missingDays = getMissingDaysFromDiff(serverDates) + if (missingDays.isNotEmpty()) { + // we have a date difference + deferredQueries.addAll( + missingDays + .map { getURLForDay(it) } + .map { url -> async { url.createDayEntryForUrl() } } + ) + } + // execute the query plan + try { + deferredQueries.awaitAll() + } catch (e: Exception) { + // For an error we clear the cache to try again + keyCache.clear() + throw e + } + keyCache.getFilesFromEntries() + .also { it.forEach { file -> Log.v(TAG, "cached file:${file.path}") } } } - keyCache.getFilesFromEntries() - .also { it.forEach { file -> Log.v(TAG, "cached file:${file.path}") } } } /** @@ -114,16 +123,18 @@ object CachedKeyFileHolder { } /** - * Calculates the missing hours based on current missing entries in the cache + * TODO remove before Release */ - private suspend fun getMissingHoursFromDiff(day: Date): List { - val cacheEntries = keyCache.getHours() - return getHoursFromServer(day) - .also { Log.v(TAG, "${it.size} hours from server") } - .filter { it.hourEntryCacheMiss(cacheEntries, day) } - .toList() - .also { Log.d(TAG, "${it.size} missing hours") } - } + private const val LATEST_HOURS_NEEDED = 3 + /** + * Calculates the last 3 hours + * TODO remove before Release + */ + private suspend fun getLast3Hours(day: Date): List = getHoursFromServer(day) + .also { Log.v(TAG, "${it.size} hours from server, but only latest 3 hours needed") } + .filter { TimeAndDateExtensions.getCurrentHourUTC() - LATEST_HOURS_NEEDED <= it.toInt() } + .toList() + .also { Log.d(TAG, "${it.size} missing hours") } /** * Determines whether a given String has an existing date cache entry under a unique name @@ -135,16 +146,6 @@ object CachedKeyFileHolder { .map { date -> date.id } .contains(getURLForDay(this).generateCacheKeyFromString()) - /** - * Determines whether a given String has an existing hour cache entry under a unique name - * given from the URL that is based on this String - * - * @param cache the given cache entries - */ - private fun String.hourEntryCacheMiss(cache: List, day: Date) = !cache - .map { hour -> hour.id } - .contains(getURLForHour(day.toServerFormat(), this).generateCacheKeyFromString()) - /** * Creates a date entry in the Key Cache for a given String with a unique Key Name derived from the URL * and the URI of the downloaded File for that given key @@ -155,16 +156,6 @@ object CachedKeyFileHolder { DAY ) - /** - * Creates an hour entry in the Key Cache for a given String with a unique Key Name derived from the URL - * and the URI of the downloaded File for that given key - */ - private suspend fun String.createHourEntryForUrl() = keyCache.createEntry( - this.generateCacheKeyFromString(), - WebRequestBuilder.asyncGetKeyFilesFromServer(this).toURI(), - HOUR - ) - /** * Generates a unique key name (UUIDv3) for the cache entry based out of a string (e.g. an url) */ @@ -199,4 +190,9 @@ object CachedKeyFileHolder { */ private suspend fun getHoursFromServer(day: Date) = WebRequestBuilder.asyncGetHourIndex(day) + + /** + * TODO remove before release + */ + private fun isLast3HourFetchEnabled(): Boolean = LocalData.last3HoursMode() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt index 6118ea66109..457de57a667 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt @@ -9,6 +9,7 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import android.os.Build import android.util.Log import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.report @@ -120,6 +121,23 @@ object ConnectivityHelper { } } + /** + * For API level 24+ check if data saver is enabled + * Else always return false + * + * @param context the context + * + * @return Boolean + * + * @see ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED + */ + fun isDataSaverEnabled(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivityManager.restrictBackgroundStatus != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED + } else false + } + /** * Get bluetooth enabled status. * diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt index 5c07fbd3315..70bcbddee65 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt @@ -34,6 +34,26 @@ object DialogHelper { positiveButtonFunction, negativeButtonFunction ) + + constructor( + activity: Activity, + title: Int, + message: String, + positiveButton: Int, + negativeButton: Int? = null, + cancelable: Boolean? = true, + positiveButtonFunction: () -> Unit? = {}, + negativeButtonFunction: () -> Unit? = {} + ) : this( + activity, + activity.resources.getString(title), + message, + activity.resources.getString(positiveButton), + negativeButton?.let { activity.resources.getString(it) }, + cancelable, + positiveButtonFunction, + negativeButtonFunction + ) } fun showDialog( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt new file mode 100644 index 00000000000..586d04069d1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.util + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +/** + * Helper to supply live data that can only be observed once + */ +open class Event(private val eventContent: T) { + private var handled = false + + fun getContent(): T? { + return if (handled) { + null + } else { + handled = true + eventContent + } + } +} + +inline fun LiveData>.observeEvent( + owner: LifecycleOwner, + crossinline onEvent: (T) -> Unit +) { + observe(owner, Observer { it?.getContent()?.let(onEvent) }) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index 7f07904ca1c..93c8ddc25ba 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -16,10 +16,6 @@ object TimeAndDateExtensions { fun getCurrentHourUTC(): Int = DateTime(Instant.now(), DateTimeZone.UTC).hourOfDay().get() - fun Date.getHourFromUTCDate(): Int = DateTime(this, DateTimeZone.UTC).hourOfDay().get() - - fun String.toMillis(): Long? = DateTime.parse(this).millis - fun Date.toServerFormat(): String = DateTimeFormat.forPattern("yyyy-MM-dd").withChronology(GJChronology.getInstance()) .withZoneUTC() @@ -31,10 +27,6 @@ object TimeAndDateExtensions { return this.div(MS_TO_SECONDS) } - fun Long.millisecondsToDays(): Long { - return this.div(MS_TO_DAYS) - } - fun Long.millisecondsToHours(): Long { return this.div(MS_TO_HOURS) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt index 29adcd8d9e2..2b26669bfa7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt @@ -255,8 +255,7 @@ fun formatTimeFetched( */ fun formatNextUpdate( riskLevelScore: Int?, - isBackgroundJobEnabled: Boolean?, - nextUpdate: Date + isBackgroundJobEnabled: Boolean? ): String { val appContext = CoronaWarnApplication.getAppContext() return if (isBackgroundJobEnabled != true) { @@ -266,9 +265,7 @@ fun formatNextUpdate( RiskLevelConstants.UNKNOWN_RISK_INITIAL, RiskLevelConstants.LOW_LEVEL_RISK, RiskLevelConstants.INCREASED_RISK -> appContext.getString( - R.string.risk_card_body_next_update, - DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM) - .format(nextUpdate) + R.string.risk_card_body_next_update ) else -> "" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt index aff12f2ac96..9194931d5c1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt @@ -105,7 +105,6 @@ fun formatNotificationsDescription(notifications: Boolean): String = formatText( /** * Formats the tracing body depending on the tracing status and the days since last exposure. * - * @param tracing * @param activeTracingDaysInRetentionPeriod * @return String */ @@ -117,6 +116,46 @@ fun formatTracingStatusBody(activeTracingDaysInRetentionPeriod: Long): String { return resources.getQuantityString(R.plurals.settings_tracing_status_body_active, days, days) } +/** + * Formats the settings notifications details illustration description depending on notifications status + * + * @param notifications + * @return + */ +fun formatNotificationIllustrationText(notifications: Boolean): String = + formatText( + notifications, + R.string.settings_notifications_illustration_description_active, + R.string.settings_notifications_illustration_description_inactive + ) + +/** + * Format the settings tracing content description for the header illustration + * + * @param tracing + * @param bluetooth + * @param connection + * @return String + */ +fun formatTracingIllustrationText( + tracing: Boolean, + bluetooth: Boolean, + connection: Boolean +): String { + val appContext = CoronaWarnApplication.getAppContext() + return when (tracingStatusHelper(tracing, bluetooth, connection)) { + TracingStatusHelper.CONNECTION -> + appContext.getString(R.string.settings_tracing_connection_illustration_description_inactive) + TracingStatusHelper.BLUETOOTH -> + appContext.getString(R.string.settings_tracing_bluetooth_illustration_description_inactive) + TracingStatusHelper.TRACING_ACTIVE -> + appContext.getString(R.string.settings_tracing_illustration_description_active) + TracingStatusHelper.TRACING_INACTIVE -> + appContext.getString(R.string.settings_tracing_illustration_description_inactive) + else -> "" + } +} + /*Styler*/ /** * Formats the settings icon color depending on flag provided diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt index 590f0cee20d..55b9a454c9e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt @@ -47,10 +47,10 @@ fun formatTestResultStatusText(uiState: DeviceUIState?): String { fun formatTestResultStatusColor(uiState: DeviceUIState?): Int { val appContext = CoronaWarnApplication.getAppContext() return when (uiState) { - DeviceUIState.PAIRED_NEGATIVE -> appContext.getColor(R.color.colorGreen) + DeviceUIState.PAIRED_NEGATIVE -> appContext.getColor(R.color.colorTextSemanticGreen) DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getColor(R.color.colorRed) - else -> appContext.getColor(R.color.colorRed) + DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getColor(R.color.colorTextSemanticRed) + else -> appContext.getColor(R.color.colorTextSemanticRed) } } @@ -162,18 +162,6 @@ fun formatSubmissionStatusCardContentVisible( uiStateState: ApiRequestState? ): Int = formatVisibility(deviceRegistered == true && uiStateState == ApiRequestState.SUCCESS) -fun formatSubmissionTanButtonTint(isValidTanFormat: Boolean) = formatColor( - isValidTanFormat, - R.color.button_primary, - R.color.colorGreyLight -) - -fun formatSubmissionTanButtonTextColor(isValidTanFormat: Boolean) = formatColor( - isValidTanFormat, - R.color.textColorLight, - R.color.colorGreyDisabled -) - fun formatShowSubmissionStatusCard(deviceUiState: DeviceUIState?): Int = formatVisibility( deviceUiState != DeviceUIState.PAIRED_POSITIVE && diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt index 799a18636a0..d8df176a766 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt @@ -19,31 +19,52 @@ package de.rki.coronawarnapp.util.security +import KeyExportFormat.TEKSignatureList import android.content.Context import android.content.SharedPreferences +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys +import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.exception.CwaSecurityException +import java.lang.Exception +import java.lang.NullPointerException import java.security.KeyStore import java.security.MessageDigest +import java.security.SecureRandom +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import java.security.Signature +import java.security.cert.Certificate /** * Key Store and Password Access */ object SecurityHelper { + private const val CWA_APP_SQLITE_DB_PW = "CWA_APP_SQLITE_DB_PW" + private const val AES_KEY_SIZE = 256 private const val SHARED_PREF_NAME = "shared_preferences_cwa" private val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC private val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) - private const val AndroidKeyStore = "AndroidKeyStore" - private val keyStore: KeyStore by lazy { - KeyStore.getInstance(AndroidKeyStore).also { + private const val EXPORT_SIGNATURE_ALGORITHM = "SHA256withECDSA" + private const val CWA_EXPORT_CERTIFICATE_NAME_NON_PROD = "cwa non-prod certificate" + + private const val CWA_EXPORT_CERTIFICATE_KEY_STORE = "trusted-certs-cwa.bks" + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + + private val androidKeyStore: KeyStore by lazy { + KeyStore.getInstance(ANDROID_KEY_STORE).also { it.load(null) } } val globalEncryptedSharedPreferencesInstance: SharedPreferences by lazy { - CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(SHARED_PREF_NAME) + withSecurityCatch { + CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(SHARED_PREF_NAME) + } } /** @@ -61,13 +82,70 @@ object SecurityHelper { /** * Retrieves the Master Key from the Android KeyStore to use in SQLCipher */ - fun getDBPassword() = keyStore - .getKey(masterKeyAlias, null) + fun getDBPassword() = getOrGenerateDBSecretKey() .toString() .toCharArray() + private fun getOrGenerateDBSecretKey(): SecretKey = + androidKeyStore.getKey(CWA_APP_SQLITE_DB_PW, null).run { + return if (this == null) { + val kg: KeyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE + ) + val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( + CWA_APP_SQLITE_DB_PW, + KeyProperties.PURPOSE_ENCRYPT and KeyProperties.PURPOSE_DECRYPT + ) + .setKeySize(AES_KEY_SIZE) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setRandomizedEncryptionRequired(true) + .setUserAuthenticationRequired(false) + .build() + kg.init(spec, SecureRandom()) + kg.generateKey() + } else this as SecretKey + } + fun hash256(input: String): String = MessageDigest .getInstance("SHA-256") .digest(input.toByteArray()) .fold("", { str, it -> str + "%02x".format(it) }) + + fun exportFileIsValid(export: ByteArray?, sig: ByteArray?) = withSecurityCatch { + Signature.getInstance(EXPORT_SIGNATURE_ALGORITHM).run { + initVerify(trustedCertForSignature) + update(export) + verify(TEKSignatureList + .parseFrom(sig) + .signaturesList + .first() + .signature + .toByteArray() + ) + } + } + + private val cwaKeyStore: KeyStore by lazy { + val keystoreFile = CoronaWarnApplication.getAppContext() + .assets.open(CWA_EXPORT_CERTIFICATE_KEY_STORE) + val keystore = KeyStore.getInstance(KeyStore.getDefaultType()) + val keyStorePw = BuildConfig.TRUSTED_CERTS_EXPORT_KEYSTORE_PW + val password = keyStorePw.toCharArray() + if (password.isEmpty()) + throw NullPointerException("TRUSTED_CERTS_EXPORT_KEYSTORE_PW is null") + keystore.load(keystoreFile, password) + keystore + } + + private val trustedCertForSignature: Certificate by lazy { + val alias = CWA_EXPORT_CERTIFICATE_NAME_NON_PROD + cwaKeyStore.getCertificate(alias) + } + + private fun withSecurityCatch(doInCatch: () -> T) = try { + doInCatch.invoke() + } catch (e: Exception) { + throw CwaSecurityException(e) + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt index 3c5c8eb804a..a36174aeac4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt @@ -38,7 +38,7 @@ object BackgroundConstants { * Total tries count for diagnosis key retrieval per day * Internal requirement */ - const val DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY = 12 + const val DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY = 1 /** * Maximum tries count for diagnosis key retrieval per day @@ -84,4 +84,9 @@ object BackgroundConstants { * @see TimeUnit.MINUTES */ const val TIME_RANGE_MAX = 1439 + + /** + * Retries before work would set as FAILED + */ + const val WORKER_RETRY_COUNT_THRESHOLD = 3 } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt index 88f2db0be3e..a40cef82d73 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.worker -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.util.Log +import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy @@ -9,14 +9,14 @@ import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Operation import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkInfo import androidx.work.WorkManager import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.TracingRepository import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.Instant +import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit /** @@ -78,11 +78,6 @@ object BackgroundWorkScheduler { BackgroundConstants.DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY .coerceAtMost(BackgroundConstants.GOOGLE_API_MAX_CALLS_PER_DAY) - /** - * Shared preferences listener - */ - private var sharedPrefListener: OnSharedPreferenceChangeListener? = null - /** * Work manager instance */ @@ -90,27 +85,44 @@ object BackgroundWorkScheduler { /** * Start work scheduler - * Subscribe shared preferences listener for changes. If any changes regarding background work - * occurred, then reschedule periodic work or stop it (depends on changes occurred). - * Two keys are monitored: - * - preference_background_jonboarding_allowed - * - preference_mobile_data_allowed - * @see LocalData.getBackgroundWorkRelatedPreferences() + * Checks if periodic worker was already scheduled. If not - reschedule it again. + * + * @see isWorkActive */ fun startWorkScheduler() { - sharedPrefListener = OnSharedPreferenceChangeListener { _, key -> - if (LocalData.getBackgroundWorkRelatedPreferences().contains(key)) { - logSharedPreferencesChange(key) - checkStart() - } else if (key == LocalData.getLastFetchDatePreference()) { - TracingRepository.refreshLastTimeDiagnosisKeysFetchedDate() + val isPeriodicWorkActive = isWorkActive(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag) + logWorkActiveStatus(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag, isPeriodicWorkActive) + if (!isPeriodicWorkActive) WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() + } + + /** + * Checks if defined work is active + * Non-active means worker was Cancelled, Failed or have not been enqueued at all + * + * @param tag String tag of the worker + * + * @return Boolean + * + * @see WorkInfo.State.CANCELLED + * @see WorkInfo.State.FAILED + */ + private fun isWorkActive(tag: String): Boolean { + val workStatus = workManager.getWorkInfosByTag(tag) + var result = true + try { + val workInfoList = workStatus.get() + if (workInfoList.size == 0) result = false + for (info in workInfoList) { + if (info.state == WorkInfo.State.CANCELLED || info.state == WorkInfo.State.FAILED) { + result = false + } } + } catch (e: ExecutionException) { + result = false + } catch (e: InterruptedException) { + result = false } - LocalData.getSharedPreferenceInstance().registerOnSharedPreferenceChangeListener( - sharedPrefListener - ) - // TODO: Reimplement after clarifications - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() + return result } /** @@ -125,31 +137,21 @@ object BackgroundWorkScheduler { } /** - * Check start periodic work - * If background work is enabled, than reschedule it. else - stop it. + * Schedule diagnosis key one time work * - * @see LocalData.isBackgroundJobEnabled() - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK + * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK */ - fun checkStart() { - if (LocalData.isBackgroundJobEnabled()) { - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.stop() - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() - } else { - stopWorkScheduler() - } + fun scheduleDiagnosisKeyPeriodicWork() { + WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() } /** * Schedule diagnosis key one time work * - * @see LocalData.isBackgroundJobEnabled() * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK */ fun scheduleDiagnosisKeyOneTimeWork() { - if (LocalData.isBackgroundJobEnabled()) { - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start() - } + WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start() } /** @@ -164,15 +166,6 @@ object BackgroundWorkScheduler { WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK -> enqueueDiagnosisKeyBackgroundOneTimeWork() } - /** - * Stop work by unique name - * - * @return Operation - * - * @see WorkType - */ - private fun WorkType.stop(): Operation = workManager.cancelUniqueWork(this.uniqueName) - /** * Enqueue diagnosis key periodic work and log it * Replace with new if older work exists. @@ -204,11 +197,13 @@ object BackgroundWorkScheduler { /** * Build diagnosis key periodic work request * Set "kind delay" for accessibility reason. + * Backoff criteria set to Linear type. * * @return PeriodicWorkRequest * * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY + * @see BackoffPolicy.LINEAR */ private fun buildDiagnosisKeyRetrievalPeriodicWork() = PeriodicWorkRequestBuilder( @@ -220,16 +215,24 @@ object BackgroundWorkScheduler { BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY, TimeUnit.MINUTES ) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY, + TimeUnit.MINUTES + ) .build() /** * Build diagnosis key one time work request * Set random initial delay for security reason. + * Backoff criteria set to Linear type. * * @return OneTimeWorkRequest * * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER * @see buildDiagnosisKeyRetrievalOneTimeWork + * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY + * @see BackoffPolicy.LINEAR */ private fun buildDiagnosisKeyRetrievalOneTimeWork() = OneTimeWorkRequestBuilder() @@ -243,6 +246,11 @@ object BackgroundWorkScheduler { ) ), TimeUnit.MINUTES ) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY, + TimeUnit.MINUTES + ) .build() /** @@ -256,29 +264,19 @@ object BackgroundWorkScheduler { /** * Constraints for diagnosis key one time work - * Depends on current application settings. + * Requires battery not low and any network connection + * Mobile data usage is handled on OS level in application settings * * @return Constraints * - * @see LocalData.isMobileDataEnabled() + * @see NetworkType.CONNECTED */ - private fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork(): Constraints { - val builder = Constraints.Builder() - if (LocalData.isMobileDataEnabled()) { - if (BuildConfig.DEBUG) Log.d( - TAG, "${WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK}:" + - "$BackgroundConstants.NETWORK_ROAMING_ALLOWED" - ) - builder.setRequiredNetworkType(NetworkType.CONNECTED) - } else { - if (BuildConfig.DEBUG) Log.d( - TAG, "${WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK}:" + - "$BackgroundConstants.NETWORK_ROAMING_FORBIDDEN" - ) - builder.setRequiredNetworkType(NetworkType.NOT_ROAMING) - } - return builder.build() - } + private fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork() = + Constraints + .Builder() + .setRequiresBatteryNotLow(true) + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() /** * Log operation schedule @@ -296,9 +294,9 @@ object BackgroundWorkScheduler { .also { if (BuildConfig.DEBUG) Log.d(TAG, "Canceling all work with tag ${workTag.tag}") } /** - * Log shared preferences change + * Log work active status */ - private fun logSharedPreferencesChange(key: String) { - if (BuildConfig.DEBUG) Log.d(TAG, "Shared preferences was changed in key: $key") + private fun logWorkActiveStatus(tag: String, active: Boolean) { + if (BuildConfig.DEBUG) Log.d(TAG, "Work type $tag is active: $active") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt index 777edc0c31b..5f31a256bc8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt @@ -5,8 +5,6 @@ import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction /** @@ -30,13 +28,18 @@ class DiagnosisKeyRetrievalOneTimeWorker(val context: Context, workerParams: Wor * @see RetrieveDiagnosisKeysTransaction */ override suspend fun doWork(): Result { - if (BuildConfig.DEBUG) Log.d(TAG, "Background job started...") + if (BuildConfig.DEBUG) Log.d(TAG, "Background job started. Run attempt: $runAttemptCount") + + if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { + if (BuildConfig.DEBUG) Log.d(TAG, "Background job failed after $runAttemptCount attempts. Rescheduling") + return Result.failure() + } + var result = Result.success() try { RetrieveDiagnosisKeysTransaction.start() } catch (e: Exception) { - e.report(ExceptionCategory.JOB) - return Result.failure() + result = Result.retry() } - return Result.success() + return result } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt index 1caccef0b73..f7d7eee4f2f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt @@ -5,8 +5,6 @@ import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report /** * Periodic diagnosis key retrieval work @@ -27,16 +25,23 @@ class DiagnosisKeyRetrievalPeriodicWorker(val context: Context, workerParams: Wo * * @return Result * + * @see BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() * @see BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() */ override suspend fun doWork(): Result { - if (BuildConfig.DEBUG) Log.d(TAG, "Background job started...") + if (BuildConfig.DEBUG) Log.d(TAG, "Background job started. Run attempt: $runAttemptCount") + + if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { + if (BuildConfig.DEBUG) Log.d(TAG, "Background job failed after $runAttemptCount attempts. Rescheduling") + BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() + return Result.failure() + } + var result = Result.success() try { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } catch (e: Exception) { - e.report(ExceptionCategory.JOB) - return Result.failure() + result = Result.retry() } - return Result.success() + return result } } diff --git a/Corona-Warn-App/src/main/res/color/button_primary.xml b/Corona-Warn-App/src/main/res/color/button_primary.xml index 11ce9cb595a..2555dd3e1ef 100644 --- a/Corona-Warn-App/src/main/res/color/button_primary.xml +++ b/Corona-Warn-App/src/main/res/color/button_primary.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/color/button_stable.xml b/Corona-Warn-App/src/main/res/color/button_stable.xml deleted file mode 100644 index bc4cd6288b4..00000000000 --- a/Corona-Warn-App/src/main/res/color/button_stable.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/color/button_stable_grey.xml b/Corona-Warn-App/src/main/res/color/button_stable_grey.xml deleted file mode 100644 index 68979507ab6..00000000000 --- a/Corona-Warn-App/src/main/res/color/button_stable_grey.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/color/button_text_color.xml b/Corona-Warn-App/src/main/res/color/button_text_color.xml new file mode 100644 index 00000000000..d99d75969c7 --- /dev/null +++ b/Corona-Warn-App/src/main/res/color/button_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml b/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml new file mode 100644 index 00000000000..b50e718df93 --- /dev/null +++ b/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/button.xml b/Corona-Warn-App/src/main/res/drawable/button.xml index bb8aff8f539..d102472376b 100644 --- a/Corona-Warn-App/src/main/res/drawable/button.xml +++ b/Corona-Warn-App/src/main/res/drawable/button.xml @@ -1,9 +1,7 @@ - - - - - - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/card.xml b/Corona-Warn-App/src/main/res/drawable/card.xml index 46d367552bd..f8eeda3b0c3 100644 --- a/Corona-Warn-App/src/main/res/drawable/card.xml +++ b/Corona-Warn-App/src/main/res/drawable/card.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/ic_information_illustration_contact.xml b/Corona-Warn-App/src/main/res/drawable/ic_information_illustration_contact.xml index ad11fa0fafb..580c4fc2057 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_information_illustration_contact.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_information_illustration_contact.xml @@ -3,79 +3,94 @@ android:height="220dp" android:viewportWidth="360" android:viewportHeight="220"> + + + + + + - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_risk_details_contact.xml b/Corona-Warn-App/src/main/res/drawable/ic_risk_details_contact.xml new file mode 100644 index 00000000000..80b69c16540 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_risk_details_contact.xml @@ -0,0 +1,12 @@ + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_intro_b.xml b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_intro_b.xml deleted file mode 100644 index e5a27ee2360..00000000000 --- a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_intro_b.xml +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_other_warning.xml b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_other_warning.xml index 2627793b076..a2892c4b4bd 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_other_warning.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_other_warning.xml @@ -1,468 +1,414 @@ + android:width="361dp" + android:height="221dp" + android:viewportWidth="361" + android:viewportHeight="221"> + + - - - - + android:strokeColor="#00000000"/> + - - - - + android:fillColor="#BE0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - + android:strokeColor="#00000000"/> - - - - + android:strokeColor="#00000000"/> + android:strokeColor="#00000000"/> - - - - - - + + + - - - - - - - - - - - - - - - - - - - - + android:fillColor="#FFFFFF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - + + + - - - - - - - - - - - - - - - - - - - - + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:strokeColor="#00000000"/> + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:strokeColor="#00000000"/> + + + + android:fillColor="#BE0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#A6B4BC" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#E8F5FF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#E8F5FF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#DCE1E5" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#657887" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#DCE1E5" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:fillColor="#657887" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + - - + + - - - + + + + + + + + + + + + + + + + + + - - + + + + + + + - - + + + android:fillColor="#EFEFEF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#BF0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#BF0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#BF0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#BF0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#BF0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#BF0F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_qr_code_card.xml b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_qr_code_card.xml index 2e43b0c7272..509a57e7765 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_qr_code_card.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_qr_code_card.xml @@ -1,2046 +1,4066 @@ + android:viewportHeight="104"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_code_card.xml b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_code_card.xml index b0ba7336a20..c89aac841f8 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_code_card.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_code_card.xml @@ -1,126 +1,102 @@ + android:viewportHeight="104"> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_hotline_card.xml b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_hotline_card.xml index f19c107d07e..5e8c2fb8119 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_hotline_card.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_submission_illustration_tan_hotline_card.xml @@ -1,45 +1,176 @@ - - - - - - - - + android:viewportHeight="104"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_invalid.xml b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_invalid.xml index 16d54eb8eb7..4e596d091d9 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_invalid.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_invalid.xml @@ -1,123 +1,80 @@ + android:width="80dp" + android:height="112dp" + android:viewportWidth="80" + android:viewportHeight="112"> + + - - - - - - - - - - - - - - - - - - - - - + android:strokeColor="#00000000"/> - + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + + + + android:fillColor="#E7E7E7" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#E7E7E7" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative.xml b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative.xml index 358b6f85c4e..d8053b5fb20 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative.xml @@ -1,132 +1,70 @@ + android:width="80dp" + android:height="112dp" + android:viewportWidth="80" + android:viewportHeight="112"> + + - - - - - - - - - - - - - + android:strokeColor="#00000000"/> - - - - - - - - - - - - + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#2E854B" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#FFFFFF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#00000000" + android:strokeColor="#FFFFFF" + android:fillType="evenOdd"/> - + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_pending.xml b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_pending.xml index 8feb68e65b6..ee5c94390ab 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_pending.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_pending.xml @@ -1,81 +1,70 @@ + android:width="80dp" + android:height="112dp" + android:viewportWidth="80" + android:viewportHeight="112"> + + - + + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - - - - - + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + - - - - - - - - - - - - + android:fillColor="#657888" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_positive.xml b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_positive.xml index d817b79c812..5e6691c4bf7 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_positive.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_positive.xml @@ -1,120 +1,130 @@ + android:width="80dp" + android:height="112dp" + android:viewportWidth="80" + android:viewportHeight="112"> + + - - - - - - - - - - - - - + android:strokeColor="#00000000"/> + + + + + + - - - - - - - - - - - - - + + + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillColor="#C00F2D" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml index d3d6d9da50d..d06cdb9200e 100644 --- a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml @@ -1,9 +1,11 @@ - - - - - - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml index 155009e579b..707b00daa06 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml @@ -39,6 +39,7 @@ android:layout_height="wrap_content" app:headline="@{@string/information_about_headline}" app:illustration="@{@drawable/ic_information_illustration_about}" + app:illustrationDescription="@{@string/information_about_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml index 54f23216215..8e84d3badd7 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml @@ -40,6 +40,7 @@ app:body="@{@string/information_contact_body}" app:headline="@{@string/information_contact_headline}" app:illustration="@{@drawable/ic_information_illustration_contact}" + app:illustrationDescription="@{@string/information_contact_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml index 03fda1730c7..9b4c6f6a339 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml @@ -39,6 +39,7 @@ android:layout_height="wrap_content" app:headline="@{@string/information_legal_headline}" app:illustration="@{@drawable/ic_information_illustration_legal}" + app:illustrationDescription="@{@string/information_legal_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml index fe958e6ffb0..5dc93cced5b 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml @@ -39,6 +39,7 @@ android:layout_height="wrap_content" app:headline="@{@string/information_privacy_headline}" app:illustration="@{@drawable/ic_information_illustration_privacy}" + app:illustrationDescription="@{@string/information_privacy_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml index 0f90814a25f..885c5dd204c 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml @@ -39,6 +39,7 @@ android:layout_height="wrap_content" app:headline="@{@string/information_technical_headline}" app:illustration="@{@drawable/ic_information_illustration_technical}" + app:illustrationDescription="@{@string/information_technical_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml index f01bbfcc3f6..4fdffed28f0 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml @@ -39,6 +39,7 @@ android:layout_height="wrap_content" app:headline="@{@string/information_terms_headline}" app:illustration="@{@drawable/ic_information_illustration_terms}" + app:illustrationDescription="@{@string/information_terms_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_main.xml b/Corona-Warn-App/src/main/res/layout/fragment_main.xml index dfaba52e58a..c1c6d292abd 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_main.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_main.xml @@ -151,6 +151,7 @@ diff --git a/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml b/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml index d364e5d2ae4..caa6b91e12a 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml @@ -49,6 +49,7 @@ app:body="@{@string/main_share_body}" app:headline="@{@string/main_share_headline}" app:illustration="@{@drawable/ic_main_illustration_share}" + app:illustrationDescription="@{@string/main_share_illustration_description}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml b/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml index 9fe27530db2..3fe9aefa85f 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml @@ -18,6 +18,7 @@ app:bodyEmphasized="@{@string/onboarding_body_emphasized}" app:headline="@{@string/onboarding_headline}" app:illustration="@{@drawable/ic_onboarding_illustration_together}" + app:illustrationDescription="@{@string/onboarding_illustration_description}" app:layout_constraintBottom_toTopOf="@+id/onboarding_button_next" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_notifications.xml b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_notifications.xml index dcad119a22b..4fac97a464b 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_notifications.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_notifications.xml @@ -24,6 +24,7 @@ app:body="@{@string/onboarding_notifications_body}" app:headline="@{@string/onboarding_notifications_headline}" app:illustration="@{@drawable/ic_onboarding_illustration_notification}" + app:illustrationDescription="@{@string/onboarding_notifications_illustration_description}" app:layout_constraintBottom_toTopOf="@+id/onboarding_button_next" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_privacy.xml b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_privacy.xml index 8cc273636c2..d210d80596e 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_privacy.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_privacy.xml @@ -25,6 +25,7 @@ app:bodyEmphasized="@{@string/onboarding_privacy_body_emphasized}" app:headline="@{@string/onboarding_privacy_headline}" app:illustration="@{@drawable/ic_onboarding_illustration_privacy}" + app:illustrationDescription="@{@string/onboarding_privacy_illustration_description}" app:layout_constraintBottom_toTopOf="@+id/onboarding_button_next" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_test.xml b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_test.xml index 13f45ca11dc..ccf9f02b0ba 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_test.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_test.xml @@ -24,6 +24,7 @@ app:body="@{@string/onboarding_test_body}" app:headline="@{@string/onboarding_test_headline}" app:illustration="@{@drawable/ic_onboarding_illustration_test}" + app:illustrationDescription="@{@string/onboarding_test_illustration_description}" app:layout_constraintBottom_toTopOf="@+id/onboarding_button_next" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_tracing.xml b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_tracing.xml index a8a4f608bf6..b2186938007 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_onboarding_tracing.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_onboarding_tracing.xml @@ -25,6 +25,7 @@ app:bodyEmphasized="@{@string/onboarding_tracing_body_emphasized}" app:headline="@{@string/onboarding_tracing_headline}" app:illustration="@{@drawable/ic_onboarding_illustration_tracing}" + app:illustrationDescription="@{@string/onboarding_tracing_illustration_description}" app:layout_constraintBottom_toTopOf="@+id/onboarding_button_next" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml b/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml index aa31ef4b5d4..c49ac7f3521 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml @@ -184,7 +184,7 @@