From 45130941b93af5226e8f80b4bf17b63d81f4aabd Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Wed, 28 Apr 2021 10:53:07 +0200 Subject: [PATCH] Incompatibility warning card on main screen (EXPOSUREAPP-5464)(COMMUNITY) (#2873) * Incompatibility warning card on main screen * Fix lint * Fix lint further * Really fix lint * Refactor BluetoothSupport.kt to be injectable, to allow mocking. * Unit test draft. * adapt tests * tests and refactoring * tests and refactoring * screenshots * add icon * add dimen again * change text * klint Co-authored-by: Fynn Godau <fynngodau@mailbox.org> Co-authored-by: chilja <chiljamgossow@gmail.com> --- .../ui/main/home/HomeFragmentTest.kt | 38 +++++++-- .../coronawarnapp/ui/main/home/HomeAdapter.kt | 2 + .../ui/main/home/HomeFragment.kt | 4 + .../ui/main/home/HomeFragmentViewModel.kt | 15 +++- .../ui/main/home/items/IncompatibleCard.kt | 42 ++++++++++ .../util/bluetooth/BluetoothSupport.kt | 60 ++++++++++++++ .../main/res/layout/home_faq_card_layout.xml | 7 +- .../layout/home_incompatible_card_layout.xml | 69 ++++++++++++++++ .../src/main/res/values-de/strings.xml | 18 ++++ .../src/main/res/values/strings.xml | 13 ++- .../main/home/HomeFragmentViewModelTest.kt | 10 ++- .../util/bluetooth/BluetoothSupportTest.kt | 82 +++++++++++++++++++ 12 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/IncompatibleCard.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupport.kt create mode 100644 Corona-Warn-App/src/main/res/layout/home_incompatible_card_layout.xml create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupportTest.kt diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt index 982637a9d..1c5536f59 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt @@ -2,9 +2,7 @@ package de.rki.coronawarnapp.ui.main.home import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module @@ -12,7 +10,6 @@ import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.statistics.source.StatisticsProvider @@ -32,6 +29,7 @@ import de.rki.coronawarnapp.ui.main.home.items.HomeItem import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.ui.statistics.Statistics import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.bluetooth.BluetoothSupport import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -65,7 +63,6 @@ class HomeFragmentTest : BaseUITest() { @MockK lateinit var tracingStateProviderFactory: TracingStateProvider.Factory @MockK lateinit var coronaTestRepository: CoronaTestRepository @MockK lateinit var tracingRepository: TracingRepository - @MockK lateinit var shareTestResultNotificationService: ShareTestResultNotificationService @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var cwaSettings: CWASettings @MockK lateinit var appConfigProvider: AppConfigProvider @@ -75,6 +72,7 @@ class HomeFragmentTest : BaseUITest() { @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var traceLocationOrganizerSettings: TraceLocationOrganizerSettings @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var bluetoothSupport: BluetoothSupport private lateinit var homeFragmentViewModel: HomeFragmentViewModel @@ -100,6 +98,9 @@ class HomeFragmentTest : BaseUITest() { override fun create(): HomeFragmentViewModel = homeFragmentViewModel } ) + + every { bluetoothSupport.isScanningSupported } returns true + every { bluetoothSupport.isAdvertisingSupported } returns true } @Screenshot @@ -111,7 +112,7 @@ class HomeFragmentTest : BaseUITest() { captureHomeFragment("low_risk_no_encounters") // also scroll down and capture a screenshot of the faq card - Espresso.onView(ViewMatchers.withId(R.id.recycler_view)).perform(recyclerScrollTo()) + onView(withId(R.id.recycler_view)).perform(recyclerScrollTo()) takeScreenshot<HomeFragment>("faq_card") } @@ -245,6 +246,30 @@ class HomeFragmentTest : BaseUITest() { } } + @Screenshot + @Test + fun captureHomeFragmentCompatibilityBleBroadcastNotSupported() { + every { homeFragmentViewModel.homeItems } returns + homeFragmentItemsLiveData(HomeData.Tracing.TRACING_FAILED_ITEM) + every { bluetoothSupport.isScanningSupported } returns true + every { bluetoothSupport.isAdvertisingSupported } returns false + launchInMainActivity<HomeFragment>() + onView(withId(R.id.recycler_view)).perform(recyclerScrollTo(2)) + captureHomeFragment("compatibility_ble_broadcast_not_supported") + } + + @Screenshot + @Test + fun captureHomeFragmentCompatibilityBleScanNotSupported() { + every { homeFragmentViewModel.homeItems } returns + homeFragmentItemsLiveData(HomeData.Tracing.TRACING_FAILED_ITEM) + every { bluetoothSupport.isScanningSupported } returns false + every { bluetoothSupport.isAdvertisingSupported } returns true + launchInMainActivity<HomeFragment>() + onView(withId(R.id.recycler_view)).perform(recyclerScrollTo(2)) + captureHomeFragment("compatibility_ble_scan_not_supported") + } + @After fun teardown() { clearAllViewModels() @@ -277,7 +302,8 @@ class HomeFragmentTest : BaseUITest() { appShortcutsHelper = appShortcutsHelper, tracingSettings = tracingSettings, traceLocationOrganizerSettings = traceLocationOrganizerSettings, - timeStamper = timeStamper + timeStamper = timeStamper, + bluetoothSupport = bluetoothSupport ) ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt index 07aafffde..66c65f4a5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt @@ -29,6 +29,7 @@ import de.rki.coronawarnapp.tracing.ui.homecards.TracingProgressCard import de.rki.coronawarnapp.ui.main.home.items.CreateTraceLocationCard import de.rki.coronawarnapp.ui.main.home.items.FAQCard import de.rki.coronawarnapp.ui.main.home.items.HomeItem +import de.rki.coronawarnapp.ui.main.home.items.IncompatibleCard import de.rki.coronawarnapp.util.lists.BindableVH import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffer @@ -50,6 +51,7 @@ class HomeAdapter : StableIdMod(data), DataBinderMod<HomeItem, HomeItemVH<HomeItem, ViewBinding>>(data), TypedVHCreatorMod({ data[it] is FAQCard.Item }) { FAQCard(it) }, + TypedVHCreatorMod({ data[it] is IncompatibleCard.Item }) { IncompatibleCard(it) }, TypedVHCreatorMod({ data[it] is CreateTraceLocationCard.Item }) { CreateTraceLocationCard(it) }, TypedVHCreatorMod({ data[it] is IncreasedRiskCard.Item }) { IncreasedRiskCard(it) }, TypedVHCreatorMod({ data[it] is LowRiskCard.Item }) { LowRiskCard(it) }, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt index dfbad3ce5..e63fe280a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -80,6 +80,10 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { ExternalActionHelper.openUrl(this@HomeFragment, getString(R.string.main_about_link)) } + viewModel.openIncompatibleEvent.observe2(this) { + ExternalActionHelper.openUrl(this@HomeFragment, getString(R.string.incompatible_link)) + } + viewModel.openTraceLocationOrganizerFlow.observe2(this) { if (viewModel.wasQRInfoWasAcknowledged()) { val nestedGraph = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index 73fc1c4d4..75c8f0e22 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -60,8 +60,10 @@ import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowTracingExplanati import de.rki.coronawarnapp.ui.main.home.items.CreateTraceLocationCard import de.rki.coronawarnapp.ui.main.home.items.FAQCard import de.rki.coronawarnapp.ui.main.home.items.HomeItem +import de.rki.coronawarnapp.ui.main.home.items.IncompatibleCard import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.bluetooth.BluetoothSupport import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper @@ -89,13 +91,15 @@ class HomeFragmentViewModel @AssistedInject constructor( private val appShortcutsHelper: AppShortcutsHelper, private val tracingSettings: TracingSettings, private val traceLocationOrganizerSettings: TraceLocationOrganizerSettings, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val bluetoothSupport: BluetoothSupport, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { private val tracingStateProvider by lazy { tracingStateProviderFactory.create(isDetailsMode = false) } val routeToScreen = SingleLiveEvent<NavDirections>() val openFAQUrlEvent = SingleLiveEvent<Unit>() + val openIncompatibleEvent = SingleLiveEvent<Unit>() val openTraceLocationOrganizerFlow = SingleLiveEvent<Unit>() val tracingHeaderState: LiveData<TracingHeaderState> = tracingStatus.generalStatus @@ -286,6 +290,15 @@ class HomeFragmentViewModel @AssistedInject constructor( else -> add(tracingItem) } + if (bluetoothSupport.isAdvertisingSupported == false) { + add( + IncompatibleCard.Item( + onClickAction = { openIncompatibleEvent.postValue(Unit) }, + bluetoothSupported = bluetoothSupport.isScanningSupported != false + ) + ) + } + // TODO: Would be nice to have a more elegant solution of displaying the result cards in the right order when (statePCR) { SubmissionStatePCR.NoTest -> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/IncompatibleCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/IncompatibleCard.kt new file mode 100644 index 000000000..5510ff442 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/IncompatibleCard.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.ui.main.home.items + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.HomeIncompatibleCardLayoutBinding +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class IncompatibleCard(parent: ViewGroup) : + HomeAdapter.HomeItemVH<IncompatibleCard.Item, HomeIncompatibleCardLayoutBinding>( + R.layout.home_card_container_layout, + parent + ) { + + override val viewBinding = lazy { + HomeIncompatibleCardLayoutBinding.inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + } + + override val onBindData: HomeIncompatibleCardLayoutBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + + when (item.bluetoothSupported) { + true -> + mainCardContentBody.setText(R.string.incompatible_advertising_not_supported) + false -> + mainCardContentBody.setText(R.string.incompatible_scanning_not_supported) + } + + itemView.setOnClickListener { + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + curItem.onClickAction(item) + } + } + + data class Item(val onClickAction: (Item) -> Unit, val bluetoothSupported: Boolean) : HomeItem, HasPayloadDiffer { + override val stableId: Long = Item::class.java.name.hashCode().toLong() + + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupport.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupport.kt new file mode 100644 index 000000000..d4ab462db --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupport.kt @@ -0,0 +1,60 @@ +package de.rki.coronawarnapp.util.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.os.Build +import dagger.Reusable +import javax.inject.Inject + +@Reusable +class BluetoothSupport @Inject constructor( + private val bluetoothAdapter: BluetoothAdapter? +) { + /** + * Determine whether Bluetooth low Energy scanning is supported + * + * @return true if supported, false if not supported, null if unknown + */ + val isScanningSupported: Boolean? + get() = when { + hasNoBluetooth -> false + isBluetoothTurnedOff -> null + hasScanner -> true + else -> false + } + + /** + * Determine whether Bluetooth Low Energy peripheral mode (advertising + * beacons) is supported + * + * @return true if supported, false if not supported, null if unknown + */ + val isAdvertisingSupported: Boolean? + get() = when { + hasNoBluetooth -> false + hasApi26AndSupportsAdvertising -> true + isBluetoothTurnedOff -> null + hasAdvertiser -> true + else -> false + } + + private val hasNoBluetooth: Boolean + get() = bluetoothAdapter == null + + private val isBluetoothTurnedOff: Boolean + get() = bluetoothAdapter?.state != BluetoothAdapter.STATE_ON + + private val hasScanner: Boolean + get() = bluetoothAdapter?.bluetoothLeScanner != null + + private val hasAdvertiser: Boolean + get() = bluetoothAdapter?.bluetoothLeAdvertiser != null + + private val hasApi26AndSupportsAdvertising: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + ( + bluetoothAdapter?.let { + it.isLeExtendedAdvertisingSupported || it.isLePeriodicAdvertisingSupported + } + ?: false + ) +} diff --git a/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml b/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml index aeb33f5ad..244403b0a 100644 --- a/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml @@ -42,10 +42,9 @@ android:layout_width="@dimen/icon_size_external_link" android:layout_height="@dimen/icon_size_external_link" android:importantForAccessibility="no" - app:srcCompat="@drawable/ic_link" - app:layout_constraintBottom_toBottomOf="@+id/main_card_header_headline" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/main_card_header_headline" /> + app:layout_constraintTop_toTopOf="@+id/main_card_header_headline" + app:srcCompat="@drawable/ic_link" /> <TextView android:id="@+id/main_card_content_body" @@ -60,4 +59,4 @@ app:layout_constraintTop_toBottomOf="@+id/main_card_header_icon" /> </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/home_incompatible_card_layout.xml b/Corona-Warn-App/src/main/res/layout/home_incompatible_card_layout.xml new file mode 100644 index 000000000..c7ecee951 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_incompatible_card_layout.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/spacing_normal" + tools:showIn="@layout/home_card_container_layout"> + + <ImageView + android:id="@+id/main_card_header_icon" + android:layout_width="@dimen/icon_size_button" + android:layout_height="@dimen/icon_size_button" + android:importantForAccessibility="no" + android:src="@drawable/ic_high_risk_alert" + app:layout_constraintBottom_toTopOf="@id/top_barrier" + app:layout_constraintEnd_toStartOf="@id/main_card_header_headline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.0" /> + + <ImageView + android:id="@+id/main_card_header_icon_end" + style="@style/icon" + android:layout_width="@dimen/icon_size_external_link" + android:layout_height="@dimen/icon_size_external_link" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/main_card_header_headline" + app:srcCompat="@drawable/ic_link" /> + + <TextView + android:id="@+id/main_card_header_headline" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_small" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:text="@string/incompatible_headline" + app:layout_constraintBottom_toTopOf="@id/top_barrier" + app:layout_constraintEnd_toStartOf="@id/main_card_header_icon_end" + app:layout_constraintStart_toEndOf="@id/main_card_header_icon" + app:layout_constraintTop_toTopOf="@id/main_card_header_icon" + app:layout_constraintVertical_bias="0.0" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/top_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="main_card_header_headline, main_card_header_icon" /> + + <TextView + android:id="@+id/main_card_content_body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:text="@string/incompatible_advertising_not_supported" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/top_barrier" /> + </androidx.constraintlayout.widget.ConstraintLayout> + +</layout> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index 83c2f1da8..afc565fa7 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -1995,4 +1995,22 @@ <string name="trace_location_attendee_invalid_qr_code_dialog_positive_button">Ok</string> <!-- XBUT: Trace location check-ins invalid qr code dialog negative button --> <string name="trace_location_attendee_invalid_qr_code_dialog_negative_button">Abbrechen</string> + + <!-- #################################### + Incompatibility warning card + ###################################### --> + + <string name="incompatible_headline">Inkompatibilitätswarnung</string> + <string name="incompatible_advertising_not_supported">Ihr Smartphone kann + COVID-19-Benachrichtigungen über die Bluetooth-Schnittstelle lediglich empfangen, jedoch + nicht versenden. Das heißt, dass Sie über diese Schnittstelle von Risiko-Begegnungen gewarnt + werden können, jedoch nicht selbst warnen können. Warnungen, die im Rahmen von Check-ins + erfolgen, können Sie sowohl empfangen als auch selbst versenden. + </string> + <string name="incompatible_scanning_not_supported">Ihr Smartphone kann + COVID-19-Benachrichtigungen über die Bluetooth-Schnittstelle weder empfangen noch senden. + Warnungen, die im Rahmen von Check-ins erfolgen, können Sie sowohl empfangen als auch selbst + versenden. + </string> + <string name="incompatible_link">"https://www.coronawarn.app/de/faq/#incompatibility_warning"</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index aea8af697..0520c47e8 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1962,7 +1962,16 @@ <!-- XACT: Button/Dialog label to cancel something--> <string name="generic_action_abort">"Cancel"</string> <!-- XACT: Button/Dialog label to remove something--> - <string name="generic_action_remove">"Remove"</string> + <string name="generic_action_remove">Remove</string> + + <!-- #################################### + Incompatibility warning card + ###################################### --> + <string name="incompatible_headline"></string> + <string name="incompatible_advertising_not_supported"></string> + <string name="incompatible_scanning_not_supported"></string> + + <string name="incompatible_link">"https://www.coronawarn.app/en/faq/#incompatibility_warning"</string> <!-- #################################### Trace Location @@ -2020,4 +2029,4 @@ <string name="trace_location_attendee_invalid_qr_code_dialog_positive_button">"OK"</string> <!-- XBUT: Trace location check-ins invalid qr code dialog negative button --> <string name="trace_location_attendee_invalid_qr_code_dialog_negative_button">"Cancel"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt index 5c027f688..9171db144 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt @@ -19,6 +19,7 @@ import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents import de.rki.coronawarnapp.ui.main.home.HomeFragmentViewModel import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.bluetooth.BluetoothSupport import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper import io.kotest.matchers.shouldBe @@ -62,6 +63,7 @@ class HomeFragmentViewModelTest : BaseTest() { @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var traceLocationOrganizerSettings: TraceLocationOrganizerSettings @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var bluetoothSupport: BluetoothSupport @BeforeEach fun setup() { @@ -78,6 +80,11 @@ class HomeFragmentViewModelTest : BaseTest() { coEvery { statisticsProvider.current } returns emptyFlow() every { timeStamper.nowUTC } returns Instant.ofEpochMilli(100101010) + + bluetoothSupport.apply { + every { isAdvertisingSupported } returns true + every { isScanningSupported } returns true + } } private fun createInstance(): HomeFragmentViewModel = HomeFragmentViewModel( @@ -95,7 +102,8 @@ class HomeFragmentViewModelTest : BaseTest() { appShortcutsHelper = appShortcutsHelper, tracingSettings = tracingSettings, traceLocationOrganizerSettings = traceLocationOrganizerSettings, - timeStamper = timeStamper + timeStamper = timeStamper, + bluetoothSupport = bluetoothSupport ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupportTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupportTest.kt new file mode 100644 index 000000000..6267c0279 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/bluetooth/BluetoothSupportTest.kt @@ -0,0 +1,82 @@ +package de.rki.coronawarnapp.util.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.le.BluetoothLeAdvertiser +import android.bluetooth.le.BluetoothLeScanner +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class BluetoothSupportTest : BaseTest() { + + @MockK lateinit var bluetoothAdapter: BluetoothAdapter + @MockK lateinit var bluetoothLeScanner: BluetoothLeScanner + @MockK lateinit var bluetoothLeAdvertiser: BluetoothLeAdvertiser + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { bluetoothAdapter.state } returns BluetoothAdapter.STATE_ON + every { bluetoothAdapter.bluetoothLeScanner } returns null + every { bluetoothAdapter.bluetoothLeAdvertiser } returns null + } + + private fun createInstance( + adapter: BluetoothAdapter? = bluetoothAdapter + ): BluetoothSupport = BluetoothSupport( + bluetoothAdapter = adapter + ) + + @Test + fun `init is side effect free`() { + createInstance() + } + + @Test + fun `scanning not supported without bluetooth`() = runBlockingTest { + createInstance(null).isScanningSupported shouldBe false + } + + @Test + fun `scanning not supported without scanner`() = runBlockingTest { + every { bluetoothAdapter.state } returns BluetoothAdapter.STATE_ON + every { bluetoothAdapter.bluetoothLeScanner } returns null + createInstance().isScanningSupported shouldBe false + } + + @Test + fun `scanning supported when turned on and has scanner`() = runBlockingTest { + every { bluetoothAdapter.state } returns BluetoothAdapter.STATE_ON + every { bluetoothAdapter.bluetoothLeScanner } returns bluetoothLeScanner + createInstance().isScanningSupported shouldBe true + } + + @Test + fun `scanning support unknown when turned off`() = runBlockingTest { + every { bluetoothAdapter.state } returns BluetoothAdapter.STATE_OFF + createInstance().isScanningSupported shouldBe null + } + + @Test + fun `advertising not supported`() = runBlockingTest { + createInstance(null).isAdvertisingSupported shouldBe false + } + + @Test + fun `advertising support unknown`() = runBlockingTest { + every { bluetoothAdapter.state } returns BluetoothAdapter.STATE_OFF + createInstance().isAdvertisingSupported shouldBe null + } + + @Test + fun `advertising supported`() = runBlockingTest { + every { bluetoothAdapter.state } returns BluetoothAdapter.STATE_ON + every { bluetoothAdapter.bluetoothLeAdvertiser } returns bluetoothLeAdvertiser + createInstance().isAdvertisingSupported shouldBe true + } +} -- GitLab