From 23498dd3c3e9ad3328fe2e285cab1943df58bda6 Mon Sep 17 00:00:00 2001 From: Mohamed <mohamed.metwalli@sap.com> Date: Thu, 4 Mar 2021 16:35:34 +0100 Subject: [PATCH] QR Code scanning to check-in (EXPOSUREAPP-5305) (#2507) * Setup deep-linking to confirmation * Update ConfirmCheckInFragment.kt * Update fragment_confrim_check_in.xml * Create Uri.kt * Add navUri extension * Handle deep-linking in MainActivity * Check event decoding * Add scan check-in qr code fragment * Create viewModel and DI modules * lint * Create ViewModel * Delegate work to view model * use viewmodel * Add unit test for navUri * Adjust data * Add unit tests for deep-linking * lint * Remove + from id references * Add TODO * Add unit tests * Add tests * add ToDo * Fix test * Deep linking for Event Registration (EXPOSUREAPP-5305) (#2502) * Handle deep-linking and shortcuts * lint * Fillin intent in Onboarding * Lint * Delete ConfirmCheckInFragment.kt * Fix test --- Corona-Warn-App/build.gradle | 1 - .../ui/launcher/LauncherActivityTest.kt | 105 +++++++++++++ .../java/de/rki/coronawarnapp/util/UriTest.kt | 45 ++++++ .../java/testhelpers/TestAppComponent.kt | 4 +- .../ui/EventRegistrationTestFragment.kt | 9 ++ .../fragment_test_eventregistration.xml | 6 + Corona-Warn-App/src/main/AndroidManifest.xml | 18 +++ .../checkins/qrcode/EventQRCode.kt | 12 +- .../EventRegistrationUIModule.kt | 18 +++ .../checkin/ConfirmCheckInEvent.kt | 6 + .../checkin/ConfirmCheckInFragment.kt | 51 ++++++ .../checkin/ConfirmCheckInModule.kt | 18 +++ .../checkin/ConfirmCheckInViewModel.kt | 44 ++++++ .../scan/ScanCheckInQrCodeEvent.kt | 6 + .../scan/ScanCheckInQrCodeFragment.kt | 147 ++++++++++++++++++ .../scan/ScanCheckInQrCodeModule.kt | 18 +++ .../scan/ScanCheckInQrCodeViewModel.kt | 25 +++ .../ui/launcher/LauncherActivity.kt | 5 +- .../rki/coronawarnapp/ui/main/MainActivity.kt | 52 ++++--- .../ui/main/MainActivityModule.kt | 4 +- .../ui/onboarding/OnboardingActivity.kt | 22 +-- .../onboarding/OnboardingLoadingFragment.kt | 2 +- .../java/de/rki/coronawarnapp/util/Uri.kt | 16 ++ .../util/shortcuts/AppShortcutsHelper.kt | 9 +- .../res/layout/fragment_confrim_check_in.xml | 54 +++++++ .../layout/fragment_scan_check_in_qr_code.xml | 96 ++++++++++++ .../src/main/res/navigation/nav_graph.xml | 23 ++- .../durationpicker/DurationExtensionKtTest.kt | 2 +- .../checkin/ConfirmCheckInViewModelTest.kt | 49 ++++++ .../scan/ScanCheckInQrCodeViewModelTest.kt | 42 +++++ 30 files changed, 851 insertions(+), 58 deletions(-) create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt create mode 100644 Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml create mode 100644 Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index e7fda93ad..dff24f01a 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -382,7 +382,6 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.ext:truth:1.3.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.work:work-testing:2.5.0' androidTestImplementation "io.mockk:mockk-android:1.10.4" debugImplementation 'androidx.fragment:fragment-testing:1.2.5' diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt new file mode 100644 index 000000000..dcc17b0f7 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt @@ -0,0 +1,105 @@ +package de.rki.coronawarnapp.ui.launcher + +import android.content.Intent +import android.net.Uri +import androidx.test.core.app.launchActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.main.CWASettings +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity +import de.rki.coronawarnapp.update.UpdateChecker +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.spyk +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseUITest +import testhelpers.TestDispatcherProvider + +@RunWith(AndroidJUnit4::class) +class LauncherActivityTest : BaseUITest() { + + @MockK lateinit var updateChecker: UpdateChecker + @MockK lateinit var cwaSettings: CWASettings + lateinit var viewModel: LauncherActivityViewModel + + @Before + fun setup() { + MockKAnnotations.init(this) + mockkObject(LocalData) + coEvery { updateChecker.checkForUpdate() } returns UpdateChecker.Result(isUpdateNeeded = false) + every { LocalData.isOnboarded() } returns false + viewModel = launcherActivityViewModel() + setupMockViewModel( + object : LauncherActivityViewModel.Factory { + override fun create(): LauncherActivityViewModel = viewModel + } + ) + + every { viewModel.events } returns mockk<SingleLiveEvent<LauncherEvent>>().apply { + every { observe(any(), any()) } just Runs + } + } + + @After + fun teardown() { + clearAllViewModels() + } + + @Test + fun testDeepLinkLowercase() { + val uri = Uri.parse("https://coronawarn.app/E1/SOME_PATH_GOES_HERE") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + @Test + fun testDeepLinkLowercaseWww() { + val uri = Uri.parse("https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + @Test(expected = RuntimeException::class) + fun testDeepLinkDoNotOpenOtherLinks() { + val uri = Uri.parse("https://www.rki.de") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + @Test(expected = RuntimeException::class) + fun testDeepLinkUppercase() { + // Host is case sensitive and it should be only in lowercase + val uri = Uri.parse("HTTPS://CORONAWARN.APP/E1/SOME_PATH_GOES_HERE") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + private fun getIntent(uri: Uri) = Intent(Intent.ACTION_VIEW, uri).apply { + setPackage(InstrumentationRegistry.getInstrumentation().targetContext.packageName) + addCategory(Intent.CATEGORY_BROWSABLE) + addCategory(Intent.CATEGORY_DEFAULT) + } + + private fun launcherActivityViewModel() = spyk( + LauncherActivityViewModel( + updateChecker, + TestDispatcherProvider(), + cwaSettings + ) + ) +} + +@Module +abstract class LauncherActivityTestModule { + @ContributesAndroidInjector + abstract fun launcherActivity(): LauncherActivity +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt new file mode 100644 index 000000000..300fc6c32 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/UriTest.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.util + +import androidx.core.net.toUri +import io.kotest.matchers.shouldBe +import org.junit.Test +import testhelpers.BaseTestInstrumentation + +class UriTest : BaseTestInstrumentation() { + + @Test + fun navUriConvertsSchemeAndAuthorityToLowercase() { + val uri = "HTTPS://CORONAWARN.APP/E1/SOME_PATH_GOES_HERE".toUri() + uri.navUri.toString() shouldBe "https://coronawarn.app/E1/SOME_PATH_GOES_HERE" + + val uri2 = "HTTPS://CORONAWARN.APP/e1/some_path_goes_here".toUri() + uri2.navUri.toString() shouldBe "https://coronawarn.app/e1/some_path_goes_here" + } + + @Test + fun navUriDoesNotChangePath() { + val uri = "https://coronawarn.app/E1/SOME_PATH_GOES_HERE".toUri() + uri.navUri.toString() shouldBe "https://coronawarn.app/E1/SOME_PATH_GOES_HERE" + + val uri2 = "https://coronawarn.app/e1/some_path_goes_here".toUri() + uri2.navUri.toString() shouldBe "https://coronawarn.app/e1/some_path_goes_here" + } + + @Test + fun navUriConvertsSchemeAndAuthorityToLowercaseWithWWW() { + val uri = "HTTPS://WWW.CORONAWARN.APP/E1/SOME_PATH_GOES_HERE".toUri() + uri.navUri.toString() shouldBe "https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE" + + val uri2 = "HTTPS://WWW.CORONAWARN.APP/e1/some_path_goes_here".toUri() + uri2.navUri.toString() shouldBe "https://www.coronawarn.app/e1/some_path_goes_here" + } + + @Test + fun navUriDoesNotChangePathWithWWW() { + val uri = "https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE".toUri() + uri.navUri.toString() shouldBe "https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE" + + val uri2 = "https://www.coronawarn.app/e1/some_path_goes_here".toUri() + uri2.navUri.toString() shouldBe "https://www.coronawarn.app/e1/some_path_goes_here" + } +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt index 67c9eb031..a5975ccf6 100644 --- a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt @@ -4,6 +4,7 @@ import dagger.BindsInstance import dagger.Component import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule +import de.rki.coronawarnapp.ui.launcher.LauncherActivityTestModule import de.rki.coronawarnapp.ui.main.MainActivityTestModule import testhelpers.viewmodels.MockViewModelModule import javax.inject.Singleton @@ -14,7 +15,8 @@ import javax.inject.Singleton MockViewModelModule::class, FragmentTestModuleRegistrar::class, TestAndroidModule::class, - MainActivityTestModule::class + MainActivityTestModule::class, + LauncherActivityTestModule::class, ] ) @Singleton diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt index de69184c9..0079ee6f3 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt @@ -1,7 +1,10 @@ package de.rki.coronawarnapp.test.eventregistration.ui import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding import de.rki.coronawarnapp.test.menu.ui.TestMenuItem @@ -19,6 +22,12 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis private val binding: FragmentTestEventregistrationBinding by viewBindingLazy() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.scanCheckInQrCode.setOnClickListener { + findNavController().navigate(R.id.scanCheckInQrCodeFragment) + } + } + companion object { val MENU_ITEM = TestMenuItem( title = "Event Registration", diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml index a80d0f83d..3fc1d7d83 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -27,6 +27,12 @@ android:layout_height="wrap_content" android:text="Event registration" /> + <com.google.android.material.button.MaterialButton + android:id="@+id/scanCheckInQrCode" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scan check in QR code" /> + </LinearLayout> </LinearLayout> diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index 8c7a9babc..e70468167 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -54,12 +54,30 @@ <activity android:name=".ui.launcher.LauncherActivity" + android:exported="true" android:screenOrientation="portrait" android:theme="@style/AppTheme.Launcher"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> + + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data + android:host="coronawarn.app" + android:pathPrefix="/" + android:scheme="https" /> + + <data + android:host="www.coronawarn.app" + android:pathPrefix="/" + android:scheme="https" /> + </intent-filter> </activity> <activity android:name=".ui.main.MainActivity" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt index 20abf0a9b..69edb248e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/EventQRCode.kt @@ -1,6 +1,10 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode -@Suppress("EmptyClassBlock") -interface EventQRCode { - // TODO -} +import org.joda.time.Instant + +data class EventQRCode( + val guid: String, + val description: String? = null, + val start: Instant? = null, + val end: Instant? = null, +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt new file mode 100644 index 000000000..e4e97fea4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.eventregistration + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInFragment +import de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInModule +import de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeFragment +import de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeModule + +@Module +internal abstract class EventRegistrationUIModule { + + @ContributesAndroidInjector(modules = [ScanCheckInQrCodeModule::class]) + abstract fun scanCheckInQrCodeFragment(): ScanCheckInQrCodeFragment + + @ContributesAndroidInjector(modules = [ConfirmCheckInModule::class]) + abstract fun confirmCheckInFragment(): ConfirmCheckInFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt new file mode 100644 index 000000000..120c3ce22 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInEvent.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.eventregistration.checkin + +sealed class ConfirmCheckInEvent { + object BackEvent : ConfirmCheckInEvent() + object ConfirmEvent : ConfirmCheckInEvent() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt new file mode 100644 index 000000000..c1389bd41 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInFragment.kt @@ -0,0 +1,51 @@ +package de.rki.coronawarnapp.ui.eventregistration.checkin + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentConfrimCheckInBinding +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject + +class ConfirmCheckInFragment : Fragment(R.layout.fragment_confrim_check_in), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: ConfirmCheckInViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentConfrimCheckInBinding by viewBindingLazy() + private val args by navArgs<ConfirmCheckInFragmentArgs>() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + toolbar.setNavigationOnClickListener { viewModel.onClose() } + confirmButton.setOnClickListener { viewModel.onConfirmEvent() } + } + + viewModel.decodeEvent(args.encodedEvent) + viewModel.navigationEvents.observe2(this) { navEvent -> + when (navEvent) { + ConfirmCheckInEvent.BackEvent -> popBackStack() + ConfirmCheckInEvent.ConfirmEvent -> popBackStack() // TODO Do something else + } + } + + // TODO bind data to actual UI + viewModel.eventData.observe2(this) { + with(binding) { + eventGuid.text = "GUID: %s".format(it.guid) + startTime.text = "Start time: %s".format(it.start) + endTime.text = "End time: %s".format(it.end) + description.text = "Description: %s".format(it.description) + } + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt new file mode 100644 index 000000000..46d8d5282 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.eventregistration.checkin + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class ConfirmCheckInModule { + @Binds + @IntoMap + @CWAViewModelKey(ConfirmCheckInViewModel::class) + abstract fun confirmCheckInFragment( + factory: ConfirmCheckInViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt new file mode 100644 index 000000000..5bcac425f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModel.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.ui.eventregistration.checkin + +import androidx.lifecycle.MutableLiveData +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode +import de.rki.coronawarnapp.eventregistration.common.decodeBase32 +import de.rki.coronawarnapp.server.protocols.internal.evreg.EventOuterClass +import de.rki.coronawarnapp.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import org.joda.time.Instant + +class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() { + private val eventLiveData = MutableLiveData<EventQRCode>() + val eventData = eventLiveData + val navigationEvents = SingleLiveEvent<ConfirmCheckInEvent>() + + fun decodeEvent(encodedEvent: String) = launch { + // TODO Verify event(EXPOSUREAPP-5423) + // and finalise event parsing logic + val decodedEventString = encodedEvent.split(".")[0].decodeBase32() + val parseEvent = EventOuterClass.Event.parseFrom(decodedEventString.toByteArray()) + eventLiveData.postValue(parseEvent.toEventQrCode()) + } + + fun onClose() { + navigationEvents.value = ConfirmCheckInEvent.BackEvent + } + + fun onConfirmEvent() { + navigationEvents.value = ConfirmCheckInEvent.ConfirmEvent + } + + private fun EventOuterClass.Event.toEventQrCode() = EventQRCode( + guid = String(guid.toByteArray()), + description = description, + start = Instant.ofEpochMilli(start.toLong()), + end = Instant.ofEpochMilli(end.toLong()) + ) + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<ConfirmCheckInViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt new file mode 100644 index 000000000..d833a241f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeEvent.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.eventregistration.scan + +sealed class ScanCheckInQrCodeEvent { + object BackEvent : ScanCheckInQrCodeEvent() + data class ConfirmCheckInEvent(val url: String) : ScanCheckInQrCodeEvent() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt new file mode 100644 index 000000000..cdf5b1045 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeFragment.kt @@ -0,0 +1,147 @@ +package de.rki.coronawarnapp.ui.eventregistration.scan + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.View +import android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT +import androidx.core.net.toUri +import androidx.navigation.fragment.findNavController +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentScanCheckInQrCodeBinding +import de.rki.coronawarnapp.util.CameraPermissionHelper +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.navUri +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject + +class ScanCheckInQrCodeFragment : + Fragment(R.layout.fragment_scan_check_in_qr_code), + AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: ScanCheckInQrCodeViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentScanCheckInQrCodeBinding by viewBindingLazy() + private var showsPermissionDialog = false + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + with(binding) { + checkInQrCodeScanTorch.setOnCheckedChangeListener { _, isChecked -> + binding.checkInQrCodeScanPreview.setTorch(isChecked) + } + checkInQrCodeScanClose.setOnClickListener { viewModel.onNavigateUp() } + checkInQrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + checkInQrCodeScanViewfinderView.setCameraPreview(binding.checkInQrCodeScanPreview) + } + + viewModel.navigationEvents.observe2(this) { navEvent -> + when (navEvent) { + is ScanCheckInQrCodeEvent.BackEvent -> popBackStack() + is ScanCheckInQrCodeEvent.ConfirmCheckInEvent -> findNavController().navigate( + navEvent.url.toUri().navUri + ) + } + } + } + + override fun onResume() { + super.onResume() + binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT) + if (CameraPermissionHelper.hasCameraPermission(requireActivity())) { + binding.checkInQrCodeScanPreview.resume() + startDecode() + return + } + if (showsPermissionDialog) return + + requestCameraPermission() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array<String>, + grantResults: IntArray + ) { + if (requestCode == REQUEST_CAMERA_PERMISSION_CODE && + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED + ) { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showCameraPermissionRationaleDialog() + } else { + // User permanently denied access to the camera + showCameraPermissionDeniedDialog() + } + } + } + + private fun startDecode() = binding.checkInQrCodeScanPreview + .decodeSingle { barcodeResult -> + viewModel.onScanResult(barcodeResult) + } + + private fun showCameraPermissionDeniedDialog() { + val permissionDeniedDialog = DialogHelper.DialogInstance( + requireActivity(), + // TODO use strings for this screen + R.string.submission_qr_code_scan_permission_denied_dialog_headline, + R.string.submission_qr_code_scan_permission_denied_dialog_body, + R.string.submission_qr_code_scan_permission_denied_dialog_button, + cancelable = false, + positiveButtonFunction = { + showsPermissionDialog = false + viewModel.onNavigateUp() + } + ) + showsPermissionDialog = true + DialogHelper.showDialog(permissionDeniedDialog) + } + + private fun showCameraPermissionRationaleDialog() { + val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance( + requireActivity(), + // TODO use strings for this screen + R.string.submission_qr_code_scan_permission_rationale_dialog_headline, + R.string.submission_qr_code_scan_permission_rationale_dialog_body, + R.string.submission_qr_code_scan_permission_rationale_dialog_button_positive, + R.string.submission_qr_code_scan_permission_rationale_dialog_button_negative, + false, + { + showsPermissionDialog = false + requestCameraPermission() + }, + { + showsPermissionDialog = false + viewModel.onNavigateUp() + } + ) + + showsPermissionDialog = true + DialogHelper.showDialog(cameraPermissionRationaleDialogInstance) + } + + private fun requestCameraPermission() = requestPermissions( + arrayOf(Manifest.permission.CAMERA), + REQUEST_CAMERA_PERMISSION_CODE + ) + + override fun onPause() { + super.onPause() + binding.checkInQrCodeScanPreview.pause() + } + + companion object { + private const val REQUEST_CAMERA_PERMISSION_CODE = 4000 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt new file mode 100644 index 000000000..64d7a0813 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.eventregistration.scan + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class ScanCheckInQrCodeModule { + @Binds + @IntoMap + @CWAViewModelKey(ScanCheckInQrCodeViewModel::class) + abstract fun scanCheckInQrCodeFragment( + factory: ScanCheckInQrCodeViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt new file mode 100644 index 000000000..a9f29b9d3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModel.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.ui.eventregistration.scan + +import com.journeyapps.barcodescanner.BarcodeResult +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class ScanCheckInQrCodeViewModel @AssistedInject constructor() : CWAViewModel() { + val navigationEvents = SingleLiveEvent<ScanCheckInQrCodeEvent>() + + fun onNavigateUp() { + navigationEvents.value = ScanCheckInQrCodeEvent.BackEvent + } + + fun onScanResult(barcodeResult: BarcodeResult) { + navigationEvents.value = ScanCheckInQrCodeEvent.ConfirmCheckInEvent( + barcodeResult.result.text + ) + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<ScanCheckInQrCodeViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt index 90c3fe4bc..27a61dabf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import javax.inject.Inject @@ -30,12 +29,12 @@ class LauncherActivity : AppCompatActivity() { vm.events.observe(this) { when (it) { LauncherEvent.GoToOnboarding -> { - OnboardingActivity.start(this, AppShortcutsHelper.getShortcutType(intent)) + OnboardingActivity.start(this, intent) this.overridePendingTransition(0, 0) finish() } LauncherEvent.GoToMainActivity -> { - MainActivity.start(this, AppShortcutsHelper.getShortcutType(intent)) + MainActivity.start(this, intent) this.overridePendingTransition(0, 0) finish() } 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 f6984c21e..4c5cc0b68 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 @@ -31,11 +31,14 @@ import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.device.PowerManagement import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.navUri +import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper.Companion.getShortcutExtra import de.rki.coronawarnapp.util.ui.findNavController import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import org.joda.time.LocalDate +import timber.log.Timber import javax.inject.Inject /** @@ -47,24 +50,14 @@ import javax.inject.Inject */ class MainActivity : AppCompatActivity(), HasAndroidInjector { companion object { - private const val EXTRA_DATA = "shortcut" - - fun start(context: Context, shortcut: AppShortcuts? = null) { - val intent = Intent(context, MainActivity::class.java).apply { - if (shortcut != null) { - putExtra(EXTRA_DATA, shortcut.toString()) - flags = flags or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK - } + fun start(context: Context, launchIntent: Intent) { + Intent(context, MainActivity::class.java).apply { + flags = flags or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK + Timber.i("launchIntent:$launchIntent") + fillIn(launchIntent, Intent.FILL_IN_DATA) + Timber.i("filledIntent:$this") + context.startActivity(this) } - context.startActivity(intent) - } - - private fun getShortcutFromIntent(intent: Intent): AppShortcuts? { - val extra = intent.getStringExtra(EXTRA_DATA) - if (extra != null) { - return AppShortcuts.valueOf(extra) - } - return null } } @@ -80,6 +73,8 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { private val FragmentManager.currentNavigationFragment: Fragment? get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first() + private val navController by lazy { supportFragmentManager.findNavController(R.id.nav_host_fragment) } + @Inject lateinit var powerManagement: PowerManagement @Inject lateinit var deadmanScheduler: DeadmanNotificationScheduler @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler @@ -105,7 +100,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { showEnergyOptimizedEnabledForBackground() } - val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment) binding.mainBottomNavigation.setupWithNavController2(navController) { vm.onBottomNavSelected() } @@ -118,16 +112,21 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + Timber.i("onNewIntent:$intent") + navigateByIntentUri(intent) + } + private fun processExtraParameters() { - when (getShortcutFromIntent(intent)) { - AppShortcuts.CONTACT_DIARY -> { - goToContactJournal() - } + when (intent.getShortcutExtra()) { + AppShortcuts.CONTACT_DIARY -> goToContactJournal() } + + navigateByIntentUri(intent) } private fun goToContactJournal() { - val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment) findViewById<BottomNavigationView>(R.id.main_bottom_navigation).selectedItemId = R.id.contact_diary_nav_graph val nestedGraph = navController.graph.findNode(R.id.contact_diary_nav_graph) as NavGraph @@ -154,6 +153,13 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { } } + private fun navigateByIntentUri(intent: Intent?) { + val uri = intent?.data ?: return + Timber.i("Uri:$uri") + Timber.i("NavUri:%s", uri.navUri) + navController.navigate(uri.navUri) + } + /** * Register callbacks. */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt index 72222d127..dc98543bb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.datadonation.analytics.ui.AnalyticsUIModule import de.rki.coronawarnapp.release.NewReleaseInfoFragment import de.rki.coronawarnapp.release.NewReleaseInfoFragmentModule import de.rki.coronawarnapp.tracing.ui.details.TracingDetailsFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.EventRegistrationUIModule import de.rki.coronawarnapp.ui.information.InformationFragmentModule import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragmentModule @@ -32,7 +33,8 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey SubmissionFragmentModule::class, InformationFragmentModule::class, NewReleaseInfoFragmentModule::class, - AnalyticsUIModule::class + AnalyticsUIModule::class, + EventRegistrationUIModule::class, ] ) abstract class MainActivityModule { 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 fe8a9252f..a020dfe53 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 @@ -15,8 +15,8 @@ import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.util.AppShortcuts import de.rki.coronawarnapp.util.di.AppInjector +import timber.log.Timber import javax.inject.Inject /** @@ -26,22 +26,16 @@ import javax.inject.Inject */ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInjector { companion object { - private val TAG: String? = OnboardingActivity::class.simpleName - private const val EXTRA_DATA = "shortcut" - fun start(context: Context, shortcut: AppShortcuts? = null) { - val intent = Intent(context, OnboardingActivity::class.java).apply { - putExtra(EXTRA_DATA, shortcut?.toString()) + fun start(context: Context, launchIntent: Intent? = null) { + val intent = Intent(context, OnboardingActivity::class.java) + Timber.i("launchIntent:$launchIntent") + launchIntent?.let { + intent.fillIn(it, Intent.FILL_IN_DATA) + Timber.i("filledIntent:$intent") } context.startActivity(intent) } - - fun getShortcutFromIntent(intent: Intent?): AppShortcuts? { - intent?.getStringExtra(EXTRA_DATA)?.let { - return AppShortcuts.valueOf(it) - } - return null - } } @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> @@ -74,7 +68,7 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj LocalData.isOnboarded(true) LocalData.onboardingCompletedTimestamp(System.currentTimeMillis()) settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } - MainActivity.start(this) + MainActivity.start(this, intent) finish() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt index 7d7c6475f..e4d0d9a7a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt @@ -41,7 +41,7 @@ class OnboardingLoadingFragment : Fragment(R.layout.onboaring_loading_layout), A .actionLoadingFragmentToOnboardingFragment() ) OnboardingFragmentEvents.OnboardingDone -> { - MainActivity.start(requireContext(), OnboardingActivity.getShortcutFromIntent(activity?.intent)) + MainActivity.start(requireContext(), requireActivity().intent) activity?.overridePendingTransition(0, 0) activity?.finish() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt new file mode 100644 index 000000000..93ab36f1c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Uri.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.util + +import android.net.Uri +import androidx.navigation.NavController +import java.util.Locale + +/** + * [NavController.navigate] by Uri is case sensitive. When authority and/or scheme are + * in Uppercase letter an Exception will thrown. + * To avoid such cases [navUri] is converting Uri schema and authority to lowercase always. + */ +val Uri.navUri: Uri + get() = Uri.Builder() + .authority(authority?.toLowerCase(Locale.ROOT)) + .scheme(scheme?.toLowerCase(Locale.ROOT)) + .path(path).build() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt index 0aeb1345d..a12907e37 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt @@ -36,18 +36,17 @@ class AppShortcutsHelper @Inject constructor(@AppContext private val context: Co private fun createContactDiaryIntent() = Intent(context, LauncherActivity::class.java).apply { action = Intent.ACTION_VIEW - putExtra(SHORTCUT_EXTRA_ID, AppShortcuts.CONTACT_DIARY.toString()) + putExtra(SHORTCUT_EXTRA, AppShortcuts.CONTACT_DIARY.toString()) } companion object { private const val CONTACT_DIARY_SHORTCUT_ID = "contact_diary_id" - private const val SHORTCUT_EXTRA_ID = "shortcut_extra" + const val SHORTCUT_EXTRA = "shortcut_extra" - fun getShortcutType(intent: Intent): AppShortcuts? { - intent.getStringExtra(SHORTCUT_EXTRA_ID)?.let { + fun Intent.getShortcutExtra(): AppShortcuts? { + getStringExtra(SHORTCUT_EXTRA)?.let { return AppShortcuts.valueOf(it) } - return null } } diff --git a/Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml b/Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml new file mode 100644 index 000000000..5f80dbeff --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_confrim_check_in.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.eventregistration.checkin.ConfirmCheckInFragment"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.Close" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <!-- TODO implement actual UI --> + <LinearLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + <TextView + android:id="@+id/eventGuid" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/startTime" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/endTime" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/confirmButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Confirm" /> + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml b/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml new file mode 100644 index 000000000..aa0768d4a --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/check_in_qr_code_scan_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/submission_qr_code_scan_title"> + + <com.journeyapps.barcodescanner.BarcodeView + android:id="@+id/check_in_qr_code_scan_preview" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:zxing_framing_rect_height="@dimen/submission_scan_qr_code_viewfinder_size" + app:zxing_framing_rect_width="@dimen/submission_scan_qr_code_viewfinder_size"> + + </com.journeyapps.barcodescanner.BarcodeView> + + <com.journeyapps.barcodescanner.ViewfinderView + android:id="@+id/check_in_qr_code_scan_viewfinder_view" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:zxing_viewfinder_laser_visibility="false" /> + + <TextView + android:id="@+id/check_in_qr_code_scan_body" + style="@style/registrationQRCodeScanBody" + android:layout_width="@dimen/submission_scan_qr_code_viewfinder_size" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/submission_scan_qr_code_viewfinder_center_offset" + android:text="@string/submission_qr_code_scan_body" + app:layout_constraintEnd_toEndOf="@id/check_in_qr_code_scan_preview" + app:layout_constraintStart_toStartOf="@id/check_in_qr_code_scan_preview" + app:layout_constraintTop_toBottomOf="@id/check_in_qr_code_scan_guideline_center" /> + + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/check_in_qr_code_scan_close" + style="@style/buttonIcon" + android:layout_width="@dimen/icon_size_button" + android:layout_height="@dimen/icon_size_button" + app:layout_constraintBottom_toTopOf="@id/check_in_qr_code_scan_guideline_top" + app:layout_constraintEnd_toStartOf="@id/guideline_start" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toTopOf="@id/check_in_qr_code_scan_guideline_top"> + + <androidx.appcompat.widget.AppCompatImageView + style="@style/iconStable" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/accessibility_close" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_close" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <ToggleButton + android:id="@+id/check_in_qr_code_scan_torch" + android:layout_width="@dimen/icon_size_button" + android:layout_height="@dimen/icon_size_button" + android:background="@drawable/ic_registration_qr_code_scan_torch_toggle" + android:backgroundTint="@color/colorStableLight" + android:textOff="" + android:textOn="" + app:layout_constraintBottom_toTopOf="@id/check_in_qr_code_scan_guideline_top" + app:layout_constraintEnd_toStartOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_end" + app:layout_constraintTop_toTopOf="@id/check_in_qr_code_scan_guideline_top" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/check_in_qr_code_scan_guideline_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_begin="@dimen/spacing_normal" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/check_in_qr_code_scan_guideline_center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.5" /> + + <include layout="@layout/merge_guidelines_side" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index b4fe423e3..7dbf6fe4d 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -534,8 +534,7 @@ android:id="@+id/surveyConsentDetailFragment" android:name="de.rki.coronawarnapp.datadonation.survey.consent.SurveyConsentDetailFragment" android:label="survey_consent_detail_fragment" - tools:layout="@layout/survey_consent_detail_fragment"> - </fragment> + tools:layout="@layout/survey_consent_detail_fragment" /> <fragment android:id="@+id/analyticsUserInputFragment" android:name="de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment" @@ -561,8 +560,7 @@ android:id="@+id/ppaMoreInfoFragment" android:name="de.rki.coronawarnapp.datadonation.analytics.ui.PpaMoreInfoFragment" android:label="PpaMoreInfoFragment" - tools:layout="@layout/fragment_ppa_more_info"> - </fragment> + tools:layout="@layout/fragment_ppa_more_info" /> <fragment android:id="@+id/settingsPrivacyPreservingAnalyticsFragment" android:name="de.rki.coronawarnapp.ui.settings.analytics.SettingsPrivacyPreservingAnalyticsFragment" @@ -575,4 +573,21 @@ android:id="@+id/action_settingsPrivacyPreservingAnalyticsFragment_to_ppaMoreInfoFragment" app:destination="@id/ppaMoreInfoFragment" /> </fragment> + + <fragment + android:id="@+id/verifyEventFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInFragment" + android:label="fragment_verify_event" + tools:layout="@layout/fragment_confrim_check_in"> + <deepLink app:uri="https://coronawarn.app/E1/{encodedEvent}" /> + <argument + android:name="encodedEvent" + app:argType="string" /> + + </fragment> + <fragment + android:id="@+id/scanCheckInQrCodeFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeFragment" + android:label="ScanCheckInQrCodeFragment" + tools:layout="@layout/fragment_scan_check_in_qr_code" /> </navigation> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt index 4bfa961ca..84d647ea2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtensionKtTest.kt @@ -54,4 +54,4 @@ internal class DurationExtensionKtTest { val suffix: String?, val expectedReadableDuration: String ) -} \ No newline at end of file +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt new file mode 100644 index 000000000..bb02e9962 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt @@ -0,0 +1,49 @@ +package de.rki.coronawarnapp.ui.eventregistration.checkin + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class ConfirmCheckInViewModelTest : BaseTest() { + + private lateinit var viewModel: ConfirmCheckInViewModel + + @BeforeEach + fun setUp() { + viewModel = ConfirmCheckInViewModel() + } + + @Test + fun decodeEvent() { + val decodedEvent = + "BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ4JCO7RVZSWVIMQK4UPA" + + ".GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ" + viewModel.decodeEvent(decodedEvent) + viewModel.eventData.getOrAwaitValue() shouldBe EventQRCode( + guid = "Lorem ipsum dolor sit amet, co", + description = "", + start = Instant.parse("1970-01-01T00:44:50.857Z"), + end = Instant.parse("1970-01-01T00:00:00.030Z") + ) + } + + @Test + fun onClose() { + viewModel.onClose() + viewModel.navigationEvents.getOrAwaitValue() shouldBe ConfirmCheckInEvent.BackEvent + } + + @Test + fun onConfirmEvent() { + viewModel.onConfirmEvent() + viewModel.navigationEvents.getOrAwaitValue() shouldBe ConfirmCheckInEvent.ConfirmEvent + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt new file mode 100644 index 000000000..e855bf055 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/scan/ScanCheckInQrCodeViewModelTest.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.ui.eventregistration.scan + +import com.google.zxing.Result +import com.journeyapps.barcodescanner.BarcodeResult +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class ScanCheckInQrCodeViewModelTest : BaseTest() { + + private lateinit var viewModel: ScanCheckInQrCodeViewModel + + @BeforeEach + fun setup() { + viewModel = ScanCheckInQrCodeViewModel() + } + + @Test + fun `onNavigateUp goes back`() { + viewModel.onNavigateUp() + viewModel.navigationEvents.getOrAwaitValue() shouldBe ScanCheckInQrCodeEvent.BackEvent + } + + @Test + fun `onScanResult results in navigation url`() { + val mockedResult = mockk<BarcodeResult>().apply { + every { result } returns mockk<Result>().apply { + every { text } returns "https://coronawarn.app/E1/SOME_PATH_GOES_HERE" + } + } + viewModel.onScanResult(mockedResult) + viewModel.navigationEvents.getOrAwaitValue() shouldBe + ScanCheckInQrCodeEvent.ConfirmCheckInEvent("https://coronawarn.app/E1/SOME_PATH_GOES_HERE") + } +} -- GitLab