From e9391c015435922d8e4949d7aedeb5b8ed974e51 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Wed, 3 Feb 2021 12:52:38 +0100 Subject: [PATCH] PPA UserInput Selection Screens (EXPOSUREAPP-4752) (#2270) * PPA User Input Strings * Data Donation: PPA User Input. First draft. TODO: Districts Tests * Fix interaction with radio button.. Fix state matching for NRW Add district mapping and display. Fix "unspecified" element position. * Test skeletons. * Finish unit tests. * Wish the LINTer a nice evening. * Move extension functions into their own file and make them public to allow label-related code to be reused on the settings screen. Additional unit tests that check the federal state short code mapping. * XML formatting. * Address additional PR comments. * Adjust radiobutton accent color. --- .../test/menu/ui/TestMenuFragmentViewModel.kt | 4 +- .../test/playground/ui/PlaygroundFragment.kt | 60 + .../test/playground/ui/PlaygroundModule.kt | 16 + .../test/playground/ui/PlaygroundViewModel.kt | 12 + .../ui/main/MainActivityTestModule.kt | 5 + .../res/layout/fragment_test_playground.xml | 30 + .../res/navigation/test_nav_graph.xml | 8 + .../ppdd-ppa-administrative-unit-set.json | 3298 +++++++++++++++++ .../analytics/AnalyticsSettings.kt | 51 +- .../analytics/common/Districts.kt | 42 + .../analytics/common/PpaDataExtensions.kt | 66 + .../analytics/ui/AnalyticsUIModule.kt | 25 + .../ui/input/AnalyticsUserInputFragment.kt | 64 + .../ui/input/AnalyticsUserInputViewModel.kt | 130 + .../analytics/ui/input/UserInfoItem.kt | 9 + .../analytics/ui/input/UserInfoItemAdapter.kt | 44 + .../ui/main/MainActivityModule.kt | 4 +- .../main/res/color/radiobutton_accented.xml | 5 + ...lytics_ppa_userinfo_input_adapter_item.xml | 31 + .../analytics_ppa_userinfo_input_fragment.xml | 26 + .../src/main/res/navigation/nav_graph.xml | 10 +- .../src/main/res/values-de/strings.xml | 56 + .../src/main/res/values/strings.xml | 56 + .../analytics/common/DistrictsTest.kt | 72 + .../analytics/common/PpaDataExtensionsTest.kt | 29 + .../analytics/ui/AnalyticsSettingsTest.kt | 79 + .../input/AnalyticsUserInputViewModelTest.kt | 180 + 27 files changed, 4408 insertions(+), 4 deletions(-) create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt create mode 100644 Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_playground.xml create mode 100644 Corona-Warn-App/src/main/assets/ppdd-ppa-administrative-unit-set.json create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/Districts.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensions.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsUIModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModel.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItem.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItemAdapter.kt create mode 100644 Corona-Warn-App/src/main/res/color/radiobutton_accented.xml create mode 100644 Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_adapter_item.xml create mode 100644 Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_fragment.xml create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/DistrictsTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensionsTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsSettingsTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModelTest.kt diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index a28ebf709..992dc6067 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment +import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment @@ -28,7 +29,8 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { SubmissionTestFragment.MENU_ITEM, SettingsCrashReportFragment.MENU_ITEM, MiscInfoFragment.MENU_ITEM, - ContactDiaryTestFragment.MENU_ITEM + ContactDiaryTestFragment.MENU_ITEM, + PlaygroundFragment.MENU_ITEM ).let { MutableLiveData(it) } } val showTestScreenEvent = SingleLiveEvent<TestMenuItem>() diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt new file mode 100644 index 000000000..82abd140c --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt @@ -0,0 +1,60 @@ +package de.rki.coronawarnapp.test.playground.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.FragmentTestPlaygroundBinding +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragmentArgs +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.di.AutoInject +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 + +@SuppressLint("SetTextI18n") +class PlaygroundFragment : Fragment(R.layout.fragment_test_playground), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val vm: PlaygroundViewModel by cwaViewModels { viewModelFactory } + private val binding: FragmentTestPlaygroundBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + dataDonationUserinfoAgegroup.setOnClickListener { + findNavController().navigate( + R.id.analyticsUserInputFragment, + AnalyticsUserInputFragmentArgs(AnalyticsUserInputFragment.InputType.AGE_GROUP).toBundle() + ) + } + dataDonationUserinfoFederalstate.setOnClickListener { + findNavController().navigate( + R.id.analyticsUserInputFragment, + AnalyticsUserInputFragmentArgs(AnalyticsUserInputFragment.InputType.FEDERAL_STATE).toBundle() + ) + } + dataDonationUserinfoDistrict.setOnClickListener { + findNavController().navigate( + R.id.analyticsUserInputFragment, + AnalyticsUserInputFragmentArgs(AnalyticsUserInputFragment.InputType.DISTRICT).toBundle() + ) + } + } + } + + companion object { + val TAG: String = PlaygroundFragment::class.simpleName!! + val MENU_ITEM = TestMenuItem( + title = "Playground", + description = "Random options for not integrated features", + targetId = R.id.playgroundFragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt new file mode 100644 index 000000000..85f6e94a9 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.test.playground.ui + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class PlaygroundModule { + @Binds + @IntoMap + @CWAViewModelKey(PlaygroundViewModel::class) + abstract fun playground(factory: PlaygroundViewModel.Factory): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt new file mode 100644 index 000000000..d411ea539 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.test.playground.ui + +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class PlaygroundViewModel @AssistedInject constructor() : CWAViewModel() { + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<PlaygroundViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt index 43dba449d..889dba938 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -14,6 +14,8 @@ import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment import de.rki.coronawarnapp.test.menu.ui.TestMenuFragmentModule +import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment +import de.rki.coronawarnapp.test.playground.ui.PlaygroundModule import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragmentModule import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment @@ -50,4 +52,7 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [ContactDiaryTestFragmentModule::class]) abstract fun contactDiaryTest(): ContactDiaryTestFragment + + @ContributesAndroidInjector(modules = [PlaygroundModule::class]) + abstract fun playground(): PlaygroundFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_playground.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_playground.xml new file mode 100644 index 000000000..5e7ffa8ec --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_playground.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> + +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="HardcodedText"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="8dp" + android:orientation="vertical"> + <Button + android:id="@+id/data_donation_userinfo_agegroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Data Donation: Age Group" /> + <Button + android:id="@+id/data_donation_userinfo_federalstate" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Data Donation: Federal State" /> + <Button + android:id="@+id/data_donation_userinfo_district" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Data Donation: District" /> + </LinearLayout> +</androidx.core.widget.NestedScrollView> diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml index 4fc364338..a07bfc226 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml @@ -37,6 +37,9 @@ <action android:id="@+id/action_test_menu_fragment_to_contactDiaryTestFragment" app:destination="@id/test_contact_diary_fragment" /> + <action + android:id="@+id/action_test_menu_fragment_to_playgroundFragment" + app:destination="@id/playgroundFragment" /> </fragment> <fragment @@ -100,5 +103,10 @@ android:name="de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment" android:label="ContactDiaryTestFragment" tools:layout="@layout/fragment_test_contact_diary" /> + <fragment + android:id="@+id/playgroundFragment" + android:name="de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment" + tools:layout="@layout/fragment_test_playground" + android:label="PlaygroundFragment" /> </navigation> diff --git a/Corona-Warn-App/src/main/assets/ppdd-ppa-administrative-unit-set.json b/Corona-Warn-App/src/main/assets/ppdd-ppa-administrative-unit-set.json new file mode 100644 index 000000000..8e43302ea --- /dev/null +++ b/Corona-Warn-App/src/main/assets/ppdd-ppa-administrative-unit-set.json @@ -0,0 +1,3298 @@ +[ + { + "districtName": "LK Dithmarschen", + "districtShortName": "HEI", + "districtId": 11001051, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Herzogtum Lauenburg", + "districtShortName": "RZ", + "districtId": 11001053, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Nordfriesland", + "districtShortName": "NF", + "districtId": 11001054, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Ostholstein", + "districtShortName": "OLD", + "districtId": 11001055, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Pinneberg", + "districtShortName": "PI", + "districtId": 11001056, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Plön", + "districtShortName": "PLÖ", + "districtId": 11001057, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Rendsburg-Eckernförde", + "districtShortName": "RD", + "districtId": 11001058, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Schleswig-Flensburg", + "districtShortName": "SL", + "districtId": 11001059, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Segeberg", + "districtShortName": "SE", + "districtId": 11001060, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Steinburg", + "districtShortName": "IZ", + "districtId": 11001061, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "LK Stormarn", + "districtShortName": "OD", + "districtId": 11001062, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "SK Flensburg", + "districtShortName": "FL", + "districtId": 11001001, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "SK Kiel", + "districtShortName": "KI", + "districtId": 11001002, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "SK Lübeck", + "districtShortName": "HL", + "districtId": 11001003, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "SK Neumünster", + "districtShortName": "NMS", + "districtId": 11001004, + "federalStateName": "Schleswig-Holstein", + "federalStateShortName": "SH", + "federalStateId": 13000001 + }, + { + "districtName": "SK Hamburg", + "districtShortName": "HH", + "districtId": 11002000, + "federalStateName": "Hamburg", + "federalStateShortName": "HH", + "federalStateId": 13000002 + }, + { + "districtName": "LK Ammerland", + "districtShortName": "WST", + "districtId": 11003451, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Aurich", + "districtShortName": "AUR", + "districtId": 11003452, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Celle", + "districtShortName": "CE", + "districtId": 11003351, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Cloppenburg", + "districtShortName": "CLP", + "districtId": 11003453, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Cuxhaven", + "districtShortName": "WEM", + "districtId": 11003352, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Diepholz", + "districtShortName": "DH", + "districtId": 11003251, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Emsland", + "districtShortName": "MEP", + "districtId": 11003454, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Friesland", + "districtShortName": "JEV", + "districtId": 11003455, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Gifhorn", + "districtShortName": "GF", + "districtId": 11003151, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Goslar", + "districtShortName": "GS", + "districtId": 11003153, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Göttingen", + "districtShortName": "GÖ", + "districtId": 11003159, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Grafschaft Bentheim", + "districtShortName": "NOH", + "districtId": 11003456, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Hameln-Pyrmont", + "districtShortName": "HM", + "districtId": 11003252, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Harburg", + "districtShortName": "WL", + "districtId": 11003353, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Heidekreis", + "districtShortName": "SOL", + "districtId": 11003358, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Helmstedt", + "districtShortName": "HE", + "districtId": 11003154, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Hildesheim", + "districtShortName": "HI", + "districtId": 11003254, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Holzminden", + "districtShortName": "HOL", + "districtId": 11003255, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Leer", + "districtShortName": "LER", + "districtId": 11003457, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Lüchow-Dannenberg", + "districtShortName": "DAN", + "districtId": 11003354, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Lüneburg", + "districtShortName": "LG", + "districtId": 11003355, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Nienburg (Weser)", + "districtShortName": "NI", + "districtId": 11003256, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Northeim", + "districtShortName": "NOM", + "districtId": 11003155, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Oldenburg", + "districtShortName": "OL", + "districtId": 11003458, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Osnabrück", + "districtShortName": "OS", + "districtId": 11003459, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Osterholz", + "districtShortName": "OHZ", + "districtId": 11003356, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Peine", + "districtShortName": "PE", + "districtId": 11003157, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Rotenburg (Wümme)", + "districtShortName": "ROW", + "districtId": 11003357, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Schaumburg", + "districtShortName": "STH", + "districtId": 11003257, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Stade", + "districtShortName": "STD", + "districtId": 11003359, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Uelzen", + "districtShortName": "UE", + "districtId": 11003360, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Vechta", + "districtShortName": "VEC", + "districtId": 11003460, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Verden", + "districtShortName": "VER", + "districtId": 11003361, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Wesermarsch", + "districtShortName": "BRA", + "districtId": 11003461, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Wittmund", + "districtShortName": "WTM", + "districtId": 11003462, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "LK Wolfenbüttel", + "districtShortName": "WF", + "districtId": 11003158, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "Region Hannover", + "districtShortName": "H", + "districtId": 11003241, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Braunschweig", + "districtShortName": "BS", + "districtId": 11003101, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Delmenhorst", + "districtShortName": "DEL", + "districtId": 11003401, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Emden", + "districtShortName": "EMD", + "districtId": 11003402, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Oldenburg", + "districtShortName": "OL", + "districtId": 11003403, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Osnabrück", + "districtShortName": "OS", + "districtId": 11003404, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Salzgitter", + "districtShortName": "SZ", + "districtId": 11003102, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Wilhelmshaven", + "districtShortName": "WHV", + "districtId": 11003405, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Wolfsburg", + "districtShortName": "WOB", + "districtId": 11003103, + "federalStateName": "Niedersachsen", + "federalStateShortName": "NI", + "federalStateId": 13000003 + }, + { + "districtName": "SK Bremen", + "districtShortName": "HB", + "districtId": 11004011, + "federalStateName": "Bremen", + "federalStateShortName": "HB", + "federalStateId": 13000004 + }, + { + "districtName": "SK Bremerhaven", + "districtShortName": "HB", + "districtId": 11004012, + "federalStateName": "Bremen", + "federalStateShortName": "HB", + "federalStateId": 13000004 + }, + { + "districtName": "LK Borken", + "districtShortName": "BOR", + "districtId": 11005554, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Coesfeld", + "districtShortName": "COE", + "districtId": 11005558, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Düren", + "districtShortName": "DN", + "districtId": 11005358, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Ennepe-Ruhr-Kreis", + "districtShortName": "EN", + "districtId": 11005954, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Euskirchen", + "districtShortName": "EU", + "districtId": 11005366, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Gütersloh", + "districtShortName": "GT", + "districtId": 11005754, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Heinsberg", + "districtShortName": "HS", + "districtId": 11005370, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Herford", + "districtShortName": "HF", + "districtId": 11005758, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Hochsauerlandkreis", + "districtShortName": "MES", + "districtId": 11005958, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Höxter", + "districtShortName": "HX", + "districtId": 11005762, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Kleve", + "districtShortName": "KLE", + "districtId": 11005154, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Lippe", + "districtShortName": "LIP", + "districtId": 11005766, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Märkischer Kreis", + "districtShortName": "MK", + "districtId": 11005962, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Mettmann", + "districtShortName": "ME", + "districtId": 11005158, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Minden-Lübbecke", + "districtShortName": "MI", + "districtId": 11005770, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Oberbergischer Kreis", + "districtShortName": "GM", + "districtId": 11005374, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Olpe", + "districtShortName": "OE", + "districtId": 11005966, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Paderborn", + "districtShortName": "PB", + "districtId": 11005774, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Recklinghausen", + "districtShortName": "RE", + "districtId": 11005562, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Rhein-Erft-Kreis", + "districtShortName": "BM", + "districtId": 11005362, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Rhein-Kreis Neuss", + "districtShortName": "NE", + "districtId": 11005162, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Rhein-Sieg-Kreis", + "districtShortName": "SU", + "districtId": 11005382, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Rheinisch-Bergischer Kreis", + "districtShortName": "GL", + "districtId": 11005378, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Siegen-Wittgenstein", + "districtShortName": "SI", + "districtId": 11005970, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Soest", + "districtShortName": "SO", + "districtId": 11005974, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Steinfurt", + "districtShortName": "ST", + "districtId": 11005566, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Unna", + "districtShortName": "UN", + "districtId": 11005978, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Viersen", + "districtShortName": "VIE", + "districtId": 11005166, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Warendorf", + "districtShortName": "WAF", + "districtId": 11005570, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Wesel", + "districtShortName": "WES", + "districtId": 11005170, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Bielefeld", + "districtShortName": "BI", + "districtId": 11005711, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Bochum", + "districtShortName": "BO", + "districtId": 11005911, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Bonn", + "districtShortName": "BN", + "districtId": 11005314, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Bottrop", + "districtShortName": "BOT", + "districtId": 11005512, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Dortmund", + "districtShortName": "DO", + "districtId": 11005913, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Duisburg", + "districtShortName": "DU", + "districtId": 11005112, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Düsseldorf", + "districtShortName": "D", + "districtId": 11005111, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Essen", + "districtShortName": "E", + "districtId": 11005113, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Gelsenkirchen", + "districtShortName": "GE", + "districtId": 11005513, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Hagen", + "districtShortName": "HA", + "districtId": 11005914, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Hamm", + "districtShortName": "HAM", + "districtId": 11005915, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Herne", + "districtShortName": "HER", + "districtId": 11005916, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Köln", + "districtShortName": "K", + "districtId": 11005315, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Krefeld", + "districtShortName": "KR", + "districtId": 11005114, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Leverkusen", + "districtShortName": "LEV", + "districtId": 11005316, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Mönchengladbach", + "districtShortName": "MG", + "districtId": 11005116, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Mülheim a.d.Ruhr", + "districtShortName": "MH", + "districtId": 11005117, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Münster", + "districtShortName": "MS", + "districtId": 11005515, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Oberhausen", + "districtShortName": "OB", + "districtId": 11005119, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Remscheid", + "districtShortName": "RS", + "districtId": 11005120, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Solingen", + "districtShortName": "SG", + "districtId": 11005122, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "SK Wuppertal", + "districtShortName": "W", + "districtId": 11005124, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "StadtRegion Aachen", + "districtShortName": "AC", + "districtId": 11005334, + "federalStateName": "Nordrhein-Westfalen", + "federalStateShortName": "NW", + "federalStateId": 13000005 + }, + { + "districtName": "LK Bergstraße", + "districtShortName": "HP", + "districtId": 11006431, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Darmstadt-Dieburg", + "districtShortName": "DI", + "districtId": 11006432, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Fulda", + "districtShortName": "FD", + "districtId": 11006631, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Gießen", + "districtShortName": "GI", + "districtId": 11006531, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Groß-Gerau", + "districtShortName": "GG", + "districtId": 11006433, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Hersfeld-Rotenburg", + "districtShortName": "HEF", + "districtId": 11006632, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Hochtaunuskreis", + "districtShortName": "HG", + "districtId": 11006434, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Kassel", + "districtShortName": "WOH", + "districtId": 11006633, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Lahn-Dill-Kreis", + "districtShortName": "LDK", + "districtId": 11006532, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Limburg-Weilburg", + "districtShortName": "LM", + "districtId": 11006533, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Main-Kinzig-Kreis", + "districtShortName": "HU", + "districtId": 11006435, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Main-Taunus-Kreis", + "districtShortName": "MTK", + "districtId": 11006436, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Marburg-Biedenkopf", + "districtShortName": "MR", + "districtId": 11006534, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Odenwaldkreis", + "districtShortName": "ERB", + "districtId": 11006437, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Offenbach", + "districtShortName": "OF", + "districtId": 11006438, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Rheingau-Taunus-Kreis", + "districtShortName": "SWA", + "districtId": 11006439, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Schwalm-Eder-Kreis", + "districtShortName": "HR", + "districtId": 11006634, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Vogelsbergkreis", + "districtShortName": "VB", + "districtId": 11006535, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Waldeck-Frankenberg", + "districtShortName": "WA", + "districtId": 11006635, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Werra-Meißner-Kreis", + "districtShortName": "ESW", + "districtId": 11006636, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Wetteraukreis", + "districtShortName": "FB", + "districtId": 11006440, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "SK Darmstadt", + "districtShortName": "DA", + "districtId": 11006411, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "SK Frankfurt am Main", + "districtShortName": "F", + "districtId": 11006412, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "SK Kassel", + "districtShortName": "KS", + "districtId": 11006611, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "SK Offenbach", + "districtShortName": "OF", + "districtId": 11006413, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "SK Wiesbaden", + "districtShortName": "WI", + "districtId": 11006414, + "federalStateName": "Hessen", + "federalStateShortName": "HE", + "federalStateId": 13000006 + }, + { + "districtName": "LK Ahrweiler", + "districtShortName": "AW", + "districtId": 11007131, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Altenkirchen", + "districtShortName": "AK", + "districtId": 11007132, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Alzey-Worms", + "districtShortName": "AZ", + "districtId": 11007331, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Bad Dürkheim", + "districtShortName": "DÜW", + "districtId": 11007332, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Bad Kreuznach", + "districtShortName": "KH", + "districtId": 11007133, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Bernkastel-Wittlich", + "districtShortName": "WIL", + "districtId": 11007231, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Birkenfeld", + "districtShortName": "BIR", + "districtId": 11007134, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Bitburg-Prüm", + "districtShortName": "BIT", + "districtId": 11007232, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Cochem-Zell", + "districtShortName": "COC", + "districtId": 11007135, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Donnersbergkreis", + "districtShortName": "KIB", + "districtId": 11007333, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Germersheim", + "districtShortName": "GER", + "districtId": 11007334, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Kaiserslautern", + "districtShortName": "KL", + "districtId": 11007335, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Kusel", + "districtShortName": "KUS", + "districtId": 11007336, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Mainz-Bingen", + "districtShortName": "MZ", + "districtId": 11007339, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Mayen-Koblenz", + "districtShortName": "MYK", + "districtId": 11007137, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Neuwied", + "districtShortName": "NR", + "districtId": 11007138, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Rhein-Hunsrück-Kreis", + "districtShortName": "SIM", + "districtId": 11007140, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Rhein-Lahn-Kreis", + "districtShortName": "EMS", + "districtId": 11007141, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Rhein-Pfalz-Kreis", + "districtShortName": "LU", + "districtId": 11007338, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Südliche Weinstraße", + "districtShortName": "SÜW", + "districtId": 11007337, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Südwestpfalz", + "districtShortName": "PS", + "districtId": 11007340, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Trier-Saarburg", + "districtShortName": "TR", + "districtId": 11007235, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Vulkaneifel", + "districtShortName": "DAU", + "districtId": 11007233, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Westerwaldkreis", + "districtShortName": "WW", + "districtId": 11007143, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Frankenthal", + "districtShortName": "FT", + "districtId": 11007311, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Kaiserslautern", + "districtShortName": "KL", + "districtId": 11007312, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Koblenz", + "districtShortName": "KO", + "districtId": 11007111, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Landau i.d.Pfalz", + "districtShortName": "LD", + "districtId": 11007313, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Ludwigshafen", + "districtShortName": "LU", + "districtId": 11007314, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Mainz", + "districtShortName": "MZ", + "districtId": 11007315, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Neustadt a.d.Weinstraße", + "districtShortName": "NW", + "districtId": 11007316, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Pirmasens", + "districtShortName": "PS", + "districtId": 11007317, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Speyer", + "districtShortName": "SP", + "districtId": 11007318, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Trier", + "districtShortName": "TR", + "districtId": 11007211, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Worms", + "districtShortName": "WO", + "districtId": 11007319, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "SK Zweibrücken", + "districtShortName": "ZW", + "districtId": 11007320, + "federalStateName": "Rheinland-Pfalz", + "federalStateShortName": "RP", + "federalStateId": 13000007 + }, + { + "districtName": "LK Alb-Donau-Kreis", + "districtShortName": "UL", + "districtId": 11008425, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Biberach", + "districtShortName": "BC", + "districtId": 11008426, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Böblingen", + "districtShortName": "BB", + "districtId": 11008115, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Bodenseekreis", + "districtShortName": "FN", + "districtId": 11008435, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Breisgau-Hochschwarzwald", + "districtShortName": "FR", + "districtId": 11008315, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Calw", + "districtShortName": "CW", + "districtId": 11008235, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Emmendingen", + "districtShortName": "EM", + "districtId": 11008316, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Enzkreis", + "districtShortName": "PF", + "districtId": 11008236, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Esslingen", + "districtShortName": "ES", + "districtId": 11008116, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Freudenstadt", + "districtShortName": "FDS", + "districtId": 11008237, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Göppingen", + "districtShortName": "GP", + "districtId": 11008117, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Heidenheim", + "districtShortName": "HDH", + "districtId": 11008135, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Heilbronn", + "districtShortName": "HN", + "districtId": 11008125, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Hohenlohekreis", + "districtShortName": "KÜN", + "districtId": 11008126, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Karlsruhe", + "districtShortName": "KA", + "districtId": 11008215, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Konstanz", + "districtShortName": "KN", + "districtId": 11008335, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Lörrach", + "districtShortName": "LÖ", + "districtId": 11008336, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Ludwigsburg", + "districtShortName": "LB", + "districtId": 11008118, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Main-Tauber-Kreis", + "districtShortName": "TBB", + "districtId": 11008128, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Neckar-Odenwald-Kreis", + "districtShortName": "MOS", + "districtId": 11008225, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Ortenaukreis", + "districtShortName": "OG", + "districtId": 11008317, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Ostalbkreis", + "districtShortName": "AA", + "districtId": 11008136, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Rastatt", + "districtShortName": "RA", + "districtId": 11008216, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Ravensburg", + "districtShortName": "RV", + "districtId": 11008436, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Rems-Murr-Kreis", + "districtShortName": "WN", + "districtId": 11008119, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Reutlingen", + "districtShortName": "RT", + "districtId": 11008415, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Rhein-Neckar-Kreis", + "districtShortName": "HD", + "districtId": 11008226, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Rottweil", + "districtShortName": "RW", + "districtId": 11008325, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Schwäbisch Hall", + "districtShortName": "SHA", + "districtId": 11008127, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Schwarzwald-Baar-Kreis", + "districtShortName": "VS", + "districtId": 11008326, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Sigmaringen", + "districtShortName": "SIG", + "districtId": 11008437, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Tübingen", + "districtShortName": "TÜ", + "districtId": 11008416, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Tuttlingen", + "districtShortName": "TUT", + "districtId": 11008327, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Waldshut", + "districtShortName": "WT", + "districtId": 11008337, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Zollernalbkreis", + "districtShortName": "BL", + "districtId": 11008417, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Baden-Baden", + "districtShortName": "BAD", + "districtId": 11008211, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Freiburg i.Breisgau", + "districtShortName": "FR", + "districtId": 11008311, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Heidelberg", + "districtShortName": "HD", + "districtId": 11008221, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Heilbronn", + "districtShortName": "HN", + "districtId": 11008121, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Karlsruhe", + "districtShortName": "KA", + "districtId": 11008212, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Mannheim", + "districtShortName": "MA", + "districtId": 11008222, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Pforzheim", + "districtShortName": "PF", + "districtId": 11008231, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Stuttgart", + "districtShortName": "S", + "districtId": 11008111, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "SK Ulm", + "districtShortName": "UL", + "districtId": 11008421, + "federalStateName": "Baden-Württemberg", + "federalStateShortName": "BW", + "federalStateId": 13000008 + }, + { + "districtName": "LK Aichach-Friedberg", + "districtShortName": "AIC", + "districtId": 11009771, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Altötting", + "districtShortName": "AÖ", + "districtId": 11009171, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Amberg-Sulzbach", + "districtShortName": "AS", + "districtId": 11009371, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Ansbach", + "districtShortName": "AN", + "districtId": 11009571, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Aschaffenburg", + "districtShortName": "AB", + "districtId": 11009671, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Augsburg", + "districtShortName": "A", + "districtId": 11009772, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Bad Kissingen", + "districtShortName": "KG", + "districtId": 11009672, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Bad Tölz-Wolfratshausen", + "districtShortName": "TÖL", + "districtId": 11009173, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Bamberg", + "districtShortName": "BA", + "districtId": 11009471, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Bayreuth", + "districtShortName": "BT", + "districtId": 11009472, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Berchtesgadener Land", + "districtShortName": "REI", + "districtId": 11009172, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Cham", + "districtShortName": "CHA", + "districtId": 11009372, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Coburg", + "districtShortName": "NEC", + "districtId": 11009473, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Dachau", + "districtShortName": "DAH", + "districtId": 11009174, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Deggendorf", + "districtShortName": "DEG", + "districtId": 11009271, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Dillingen a.d.Donau", + "districtShortName": "DLG", + "districtId": 11009773, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Dingolfing-Landau", + "districtShortName": "LAN", + "districtId": 11009279, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Donau-Ries", + "districtShortName": "DON", + "districtId": 11009779, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Ebersberg", + "districtShortName": "EBE", + "districtId": 11009175, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Eichstätt", + "districtShortName": "EIH", + "districtId": 11009176, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Erding", + "districtShortName": "ED", + "districtId": 11009177, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Erlangen-Höchstadt", + "districtShortName": "ERL", + "districtId": 11009572, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Forchheim", + "districtShortName": "FO", + "districtId": 11009474, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Freising", + "districtShortName": "FS", + "districtId": 11009178, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Freyung-Grafenau", + "districtShortName": "WOS", + "districtId": 11009272, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Fürstenfeldbruck", + "districtShortName": "FFB", + "districtId": 11009179, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Fürth", + "districtShortName": "FÜ", + "districtId": 11009573, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Garmisch-Partenkirchen", + "districtShortName": "GAP", + "districtId": 11009180, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Günzburg", + "districtShortName": "GZ", + "districtId": 11009774, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Haßberge", + "districtShortName": "HAS", + "districtId": 11009674, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Hof", + "districtShortName": "HO", + "districtId": 11009475, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Kelheim", + "districtShortName": "KEH", + "districtId": 11009273, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Kitzingen", + "districtShortName": "KT", + "districtId": 11009675, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Kronach", + "districtShortName": "KC", + "districtId": 11009476, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Kulmbach", + "districtShortName": "KU", + "districtId": 11009477, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Landsberg a.Lech", + "districtShortName": "LL", + "districtId": 11009181, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Landshut", + "districtShortName": "LA", + "districtId": 11009274, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Lichtenfels", + "districtShortName": "LIF", + "districtId": 11009478, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Lindau", + "districtShortName": "LI", + "districtId": 11009776, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Main-Spessart", + "districtShortName": "MSP", + "districtId": 11009677, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Miesbach", + "districtShortName": "MB", + "districtId": 11009182, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Miltenberg", + "districtShortName": "MIL", + "districtId": 11009676, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Mühldorf a.Inn", + "districtShortName": "MÜ", + "districtId": 11009183, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK München", + "districtShortName": "M", + "districtId": 11009184, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Neu-Ulm", + "districtShortName": "NU", + "districtId": 11009775, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Neuburg-Schrobenhausen", + "districtShortName": "ND", + "districtId": 11009185, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Neumarkt i.d.OPf.", + "districtShortName": "NM", + "districtId": 11009373, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Neustadt a.d.Aisch-Bad Windsheim", + "districtShortName": "NEA", + "districtId": 11009575, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Neustadt a.d.Waldnaab", + "districtShortName": "NEW", + "districtId": 11009374, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Nürnberger Land", + "districtShortName": "LAU", + "districtId": 11009574, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Oberallgäu", + "districtShortName": "SF", + "districtId": 11009780, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Ostallgäu", + "districtShortName": "OAL", + "districtId": 11009777, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Passau", + "districtShortName": "PA", + "districtId": 11009275, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Pfaffenhofen a.d.Ilm", + "districtShortName": "PAF", + "districtId": 11009186, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Regen", + "districtShortName": "REG", + "districtId": 11009276, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Regensburg", + "districtShortName": "R", + "districtId": 11009375, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Rhön-Grabfeld", + "districtShortName": "NES", + "districtId": 11009673, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Rosenheim", + "districtShortName": "RO", + "districtId": 11009187, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Roth", + "districtShortName": "RH", + "districtId": 11009576, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Rottal-Inn", + "districtShortName": "PAN", + "districtId": 11009277, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Schwandorf", + "districtShortName": "SAD", + "districtId": 11009376, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Schweinfurt", + "districtShortName": "SW", + "districtId": 11009678, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Starnberg", + "districtShortName": "STA", + "districtId": 11009188, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Straubing-Bogen", + "districtShortName": "SR", + "districtId": 11009278, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Tirschenreuth", + "districtShortName": "TIR", + "districtId": 11009377, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Traunstein", + "districtShortName": "TS", + "districtId": 11009189, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Unterallgäu", + "districtShortName": "MN", + "districtId": 11009778, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Weilheim-Schongau", + "districtShortName": "WM", + "districtId": 11009190, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Weißenburg-Gunzenhausen", + "districtShortName": "WUG", + "districtId": 11009577, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Wunsiedel i.Fichtelgebirge", + "districtShortName": "WUN", + "districtId": 11009479, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Würzburg", + "districtShortName": "WÜ", + "districtId": 11009679, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Amberg", + "districtShortName": "AM", + "districtId": 11009361, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Ansbach", + "districtShortName": "AN", + "districtId": 11009561, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Aschaffenburg", + "districtShortName": "AB", + "districtId": 11009661, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Augsburg", + "districtShortName": "A", + "districtId": 11009761, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Bamberg", + "districtShortName": "BA", + "districtId": 11009461, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Bayreuth", + "districtShortName": "BT", + "districtId": 11009462, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Coburg", + "districtShortName": "CO", + "districtId": 11009463, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Erlangen", + "districtShortName": "ER", + "districtId": 11009562, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Fürth", + "districtShortName": "FÜ", + "districtId": 11009563, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Hof", + "districtShortName": "HO", + "districtId": 11009464, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Ingolstadt", + "districtShortName": "IN", + "districtId": 11009161, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Kaufbeuren", + "districtShortName": "KF", + "districtId": 11009762, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Kempten", + "districtShortName": "KE", + "districtId": 11009763, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Landshut", + "districtShortName": "LA", + "districtId": 11009261, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Memmingen", + "districtShortName": "MM", + "districtId": 11009764, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK München", + "districtShortName": "M", + "districtId": 11009162, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Nürnberg", + "districtShortName": "N", + "districtId": 11009564, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Passau", + "districtShortName": "PA", + "districtId": 11009262, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Regensburg", + "districtShortName": "R", + "districtId": 11009362, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Rosenheim", + "districtShortName": "RO", + "districtId": 11009163, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Schwabach", + "districtShortName": "SC", + "districtId": 11009565, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Schweinfurt", + "districtShortName": "SW", + "districtId": 11009662, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Straubing", + "districtShortName": "SR", + "districtId": 11009263, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Weiden i.d.OPf.", + "districtShortName": "WEN", + "districtId": 11009363, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "SK Würzburg", + "districtShortName": "WÜ", + "districtId": 11009663, + "federalStateName": "Bayern", + "federalStateShortName": "BY", + "federalStateId": 13000009 + }, + { + "districtName": "LK Merzig-Wadern", + "districtShortName": "MZG", + "districtId": 11010042, + "federalStateName": "Saarland", + "federalStateShortName": "SL", + "federalStateId": 13000010 + }, + { + "districtName": "LK Neunkirchen", + "districtShortName": "NK", + "districtId": 11010043, + "federalStateName": "Saarland", + "federalStateShortName": "SL", + "federalStateId": 13000010 + }, + { + "districtName": "LK Saarpfalz-Kreis", + "districtShortName": "HOM", + "districtId": 11010045, + "federalStateName": "Saarland", + "federalStateShortName": "SL", + "federalStateId": 13000010 + }, + { + "districtName": "LK Saarlouis", + "districtShortName": "SLS", + "districtId": 11010044, + "federalStateName": "Saarland", + "federalStateShortName": "SL", + "federalStateId": 13000010 + }, + { + "districtName": "LK Sankt Wendel", + "districtShortName": "WND", + "districtId": 11010046, + "federalStateName": "Saarland", + "federalStateShortName": "SL", + "federalStateId": 13000010 + }, + { + "districtName": "LK Stadtverband Saarbrücken", + "districtShortName": "SB", + "districtId": 11010041, + "federalStateName": "Saarland", + "federalStateShortName": "SL", + "federalStateId": 13000010 + }, + { + "districtName": "SK Berlin Charlottenburg-Wilmersdorf", + "districtShortName": "B", + "districtId": 11011004, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Friedrichshain-Kreuzberg", + "districtShortName": "B", + "districtId": 11011002, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Lichtenberg", + "districtShortName": "B", + "districtId": 11011011, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Marzahn-Hellersdorf", + "districtShortName": "B", + "districtId": 11011010, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Mitte", + "districtShortName": "B", + "districtId": 11011001, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Neukölln", + "districtShortName": "B", + "districtId": 11011008, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Pankow", + "districtShortName": "B", + "districtId": 11011003, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Reinickendorf", + "districtShortName": "B", + "districtId": 11011012, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Spandau", + "districtShortName": "B", + "districtId": 11011005, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Steglitz-Zehlendorf", + "districtShortName": "B", + "districtId": 11011006, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Tempelhof-Schöneberg", + "districtShortName": "B", + "districtId": 11011007, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "SK Berlin Treptow-Köpenick", + "districtShortName": "B", + "districtId": 11011009, + "federalStateName": "Berlin", + "federalStateShortName": "BE", + "federalStateId": 13000011 + }, + { + "districtName": "LK Barnim", + "districtShortName": "BAR", + "districtId": 11012060, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Dahme-Spreewald", + "districtShortName": "LDS", + "districtId": 11012061, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Elbe-Elster", + "districtShortName": "EE", + "districtId": 11012062, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Havelland", + "districtShortName": "HVL", + "districtId": 11012063, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Märkisch-Oderland", + "districtShortName": "MOL", + "districtId": 11012064, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Oberhavel", + "districtShortName": "OHV", + "districtId": 11012065, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Oberspreewald-Lausitz", + "districtShortName": "OSL", + "districtId": 11012066, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Oder-Spree", + "districtShortName": "LOS", + "districtId": 11012067, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Ostprignitz-Ruppin", + "districtShortName": "OPR", + "districtId": 11012068, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Potsdam-Mittelmark", + "districtShortName": "PM", + "districtId": 11012069, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Prignitz", + "districtShortName": "PR", + "districtId": 11012070, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Spree-Neiße", + "districtShortName": "SPN", + "districtId": 11012071, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Teltow-Fläming", + "districtShortName": "TF", + "districtId": 11012072, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Uckermark", + "districtShortName": "UM", + "districtId": 11012073, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "SK Brandenburg a.d.Havel", + "districtShortName": "BRB", + "districtId": 11012051, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "SK Cottbus", + "districtShortName": "CB", + "districtId": 11012052, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "SK Frankfurt (Oder)", + "districtShortName": "FF", + "districtId": 11012053, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "SK Potsdam", + "districtShortName": "P", + "districtId": 11012054, + "federalStateName": "Brandenburg", + "federalStateShortName": "BB", + "federalStateId": 13000012 + }, + { + "districtName": "LK Ludwigslust-Parchim", + "districtShortName": "NULL", + "districtId": 11013076, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "LK Mecklenburgische Seenplatte", + "districtShortName": "NULL", + "districtId": 11013071, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "LK Nordwestmecklenburg", + "districtShortName": "NULL", + "districtId": 11013074, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "LK Vorpommern-Rügen", + "districtShortName": "NULL", + "districtId": 11013073, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "LK Vorpommern-Greifswald", + "districtShortName": "NULL", + "districtId": 11013075, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "LK Rostock", + "districtShortName": "NULL", + "districtId": 11013072, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "SK Rostock", + "districtShortName": "HRO", + "districtId": 11013003, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "SK Schwerin", + "districtShortName": "SN", + "districtId": 11013004, + "federalStateName": "Mecklenburg-Vorpommern", + "federalStateShortName": "MV", + "federalStateId": 13000013 + }, + { + "districtName": "LK Bautzen", + "districtShortName": "BZ", + "districtId": 11014625, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Erzgebirgskreis", + "districtShortName": "ERZ", + "districtId": 11014521, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Görlitz", + "districtShortName": "GR", + "districtId": 11014626, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Leipzig", + "districtShortName": "L", + "districtId": 11014729, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Meißen", + "districtShortName": "MEI", + "districtId": 11014627, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Mittelsachsen", + "districtShortName": "FG", + "districtId": 11014522, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Nordsachsen", + "districtShortName": "TDO", + "districtId": 11014730, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Sächsische Schweiz-Osterzgebirge", + "districtShortName": "PIR", + "districtId": 11014628, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Vogtlandkreis", + "districtShortName": "V", + "districtId": 11014523, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Zwickau", + "districtShortName": "Z", + "districtId": 11014524, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "SK Chemnitz", + "districtShortName": "C", + "districtId": 11014511, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "SK Dresden", + "districtShortName": "DD", + "districtId": 11014612, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "SK Leipzig", + "districtShortName": "L", + "districtId": 11014713, + "federalStateName": "Sachsen", + "federalStateShortName": "SN", + "federalStateId": 13000014 + }, + { + "districtName": "LK Altmarkkreis Salzwedel", + "districtShortName": "SAW", + "districtId": 11015081, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Anhalt-Bitterfeld", + "districtShortName": "BTF", + "districtId": 11015082, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Börde", + "districtShortName": "BÖ", + "districtId": 11015083, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Burgenlandkreis", + "districtShortName": "BLK", + "districtId": 11015084, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Harz", + "districtShortName": "HZ", + "districtId": 11015085, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Jerichower Land", + "districtShortName": "JL", + "districtId": 11015086, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Mansfeld-Südharz", + "districtShortName": "ML", + "districtId": 11015087, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Saalekreis", + "districtShortName": "SK", + "districtId": 11015088, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Salzlandkreis", + "districtShortName": "SLK", + "districtId": 11015089, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Stendal", + "districtShortName": "SDL", + "districtId": 11015090, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Wittenberg", + "districtShortName": "WB", + "districtId": 11015091, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "SK Dessau-Roßlau", + "districtShortName": "DE", + "districtId": 11015001, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "SK Halle", + "districtShortName": "HAL", + "districtId": 11015002, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "SK Magdeburg", + "districtShortName": "MD", + "districtId": 11015003, + "federalStateName": "Sachsen-Anhalt", + "federalStateShortName": "ST", + "federalStateId": 13000015 + }, + { + "districtName": "LK Altenburger Land", + "districtShortName": "ABG", + "districtId": 11016077, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Eichsfeld", + "districtShortName": "EIC", + "districtId": 11016061, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Gotha", + "districtShortName": "GTH", + "districtId": 11016067, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Greiz", + "districtShortName": "GRZ", + "districtId": 11016076, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Hildburghausen", + "districtShortName": "SHL", + "districtId": 11016069, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Ilm-Kreis", + "districtShortName": "SHL", + "districtId": 11016070, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Kyffhäuserkreis", + "districtShortName": "KYF", + "districtId": 11016065, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Nordhausen", + "districtShortName": "NDH", + "districtId": 11016062, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Saale-Holzland-Kreis", + "districtShortName": "SHK", + "districtId": 11016074, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Saale-Orla-Kreis", + "districtShortName": "SOK", + "districtId": 11016075, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Saalfeld-Rudolstadt", + "districtShortName": "SLF", + "districtId": 11016073, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Schmalkalden-Meiningen", + "districtShortName": "SM", + "districtId": 11016066, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Sömmerda", + "districtShortName": "SÖM", + "districtId": 11016068, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Sonneberg", + "districtShortName": "SON", + "districtId": 11016072, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Unstrut-Hainich-Kreis", + "districtShortName": "UH", + "districtId": 11016064, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Wartburgkreis", + "districtShortName": "WAK", + "districtId": 11016063, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "LK Weimarer Land", + "districtShortName": "WE", + "districtId": 11016071, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "SK Eisenach", + "districtShortName": "EA", + "districtId": 11016056, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "SK Erfurt", + "districtShortName": "EF", + "districtId": 11016051, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "SK Gera", + "districtShortName": "G", + "districtId": 11016052, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "SK Jena", + "districtShortName": "J", + "districtId": 11016053, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "SK Suhl", + "districtShortName": "SHL", + "districtId": 11016054, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + }, + { + "districtName": "SK Weimar", + "districtShortName": "WE", + "districtId": 11016055, + "federalStateName": "Thüringen", + "federalStateShortName": "TH", + "federalStateId": 13000016 + } +] \ No newline at end of file diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsSettings.kt index 3f8eaeb25..baaaec637 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsSettings.kt @@ -1,7 +1,56 @@ package de.rki.coronawarnapp.datadonation.analytics +import android.content.Context +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.createFlowPreference import javax.inject.Inject import javax.inject.Singleton @Singleton -class AnalyticsSettings @Inject constructor() +class AnalyticsSettings @Inject constructor( + @AppContext private val context: Context +) { + private val prefs by lazy { + context.getSharedPreferences("analytics_localdata", Context.MODE_PRIVATE) + } + + val userInfoAgeGroup = prefs.createFlowPreference( + key = PKEY_USERINFO_AGEGROUP, + reader = { key -> + PpaData.PPAAgeGroup.forNumber(getInt(key, 0)) ?: PpaData.PPAAgeGroup.AGE_GROUP_UNSPECIFIED + }, + writer = { key, value -> + val numberToWrite = when (value) { + PpaData.PPAAgeGroup.UNRECOGNIZED -> PpaData.PPAAgeGroup.AGE_GROUP_UNSPECIFIED.number + else -> value.number + } + putInt(key, numberToWrite) + } + ) + + val userInfoFederalState = prefs.createFlowPreference( + key = PKEY_USERINFO_FEDERALSTATE, + reader = { key -> + PpaData.PPAFederalState.forNumber(getInt(key, -1)) ?: PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED + }, + writer = { key, value -> + val numberToWrite = when (value) { + PpaData.PPAFederalState.UNRECOGNIZED -> PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED.number + else -> value.number + } + putInt(key, numberToWrite) + } + ) + + val userInfoDistrict = prefs.createFlowPreference( + key = PKEY_USERINFO_DISTRICT, + defaultValue = 0 + ) + + companion object { + private const val PKEY_USERINFO_AGEGROUP = "userinfo.agegroup" + private const val PKEY_USERINFO_FEDERALSTATE = "userinfo.federalstate" + private const val PKEY_USERINFO_DISTRICT = "userinfo.district" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/Districts.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/Districts.kt new file mode 100644 index 000000000..f7d743ac1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/Districts.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.datadonation.analytics.common + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import dagger.Reusable +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.fromJson +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class Districts @Inject constructor( + @AppContext private val context: Context, + @BaseGson private val gson: Gson +) { + + suspend fun loadDistricts(): List<District> { + return try { + val rawDistricts = context.assets.open(ASSET_NAME).bufferedReader().use { it.readText() } + gson.fromJson(rawDistricts) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to parse districts.") + emptyList() + } + } + + data class District( + @SerializedName("districtName") val districtName: String = "", + @SerializedName("districtShortName") val districtShortName: String = "", + @SerializedName("districtId") val districtId: Int = 0, + @SerializedName("federalStateName") val federalStateName: String = "", + @SerializedName("federalStateShortName") val federalStateShortName: String = "", + @SerializedName("federalStateId") val federalStateId: Int = 0 + ) + + companion object { + private const val ASSET_NAME = "ppdd-ppa-administrative-unit-set.json" + private const val TAG = "Districts" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensions.kt new file mode 100644 index 000000000..5a24f909c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensions.kt @@ -0,0 +1,66 @@ +package de.rki.coronawarnapp.datadonation.analytics.common + +import androidx.annotation.StringRes +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData + +val PpaData.PPAAgeGroup.labelStringRes: Int + @StringRes + get() = when (this) { + PpaData.PPAAgeGroup.AGE_GROUP_UNSPECIFIED -> R.string.analytics_userinput_agegroup_unspecified + PpaData.PPAAgeGroup.AGE_GROUP_0_TO_29 -> R.string.analytics_userinput_agegroup_0_to_29 + PpaData.PPAAgeGroup.AGE_GROUP_30_TO_59 -> R.string.analytics_userinput_agegroup_30_to_59 + PpaData.PPAAgeGroup.AGE_GROUP_FROM_60 -> R.string.analytics_userinput_agegroup_from_60 + PpaData.PPAAgeGroup.UNRECOGNIZED -> throw UnsupportedOperationException( + "PpaData.PPAAgeGroup.UNRECOGNIZED has no label." + ) + } + +val PpaData.PPAFederalState.labelStringRes: Int + @StringRes + get() = when (this) { + PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED -> R.string.analytics_userinput_federalstate_unspecified + PpaData.PPAFederalState.FEDERAL_STATE_BW -> R.string.analytics_userinput_federalstate_bw + PpaData.PPAFederalState.FEDERAL_STATE_BY -> R.string.analytics_userinput_federalstate_by + PpaData.PPAFederalState.FEDERAL_STATE_BE -> R.string.analytics_userinput_federalstate_be + PpaData.PPAFederalState.FEDERAL_STATE_BB -> R.string.analytics_userinput_federalstate_bb + PpaData.PPAFederalState.FEDERAL_STATE_HB -> R.string.analytics_userinput_federalstate_hb + PpaData.PPAFederalState.FEDERAL_STATE_HH -> R.string.analytics_userinput_federalstate_hh + PpaData.PPAFederalState.FEDERAL_STATE_HE -> R.string.analytics_userinput_federalstate_he + PpaData.PPAFederalState.FEDERAL_STATE_MV -> R.string.analytics_userinput_federalstate_mv + PpaData.PPAFederalState.FEDERAL_STATE_NI -> R.string.analytics_userinput_federalstate_ni + PpaData.PPAFederalState.FEDERAL_STATE_NRW -> R.string.analytics_userinput_federalstate_nrw + PpaData.PPAFederalState.FEDERAL_STATE_RP -> R.string.analytics_userinput_federalstate_rp + PpaData.PPAFederalState.FEDERAL_STATE_SL -> R.string.analytics_userinput_federalstate_sl + PpaData.PPAFederalState.FEDERAL_STATE_SN -> R.string.analytics_userinput_federalstate_sn + PpaData.PPAFederalState.FEDERAL_STATE_ST -> R.string.analytics_userinput_federalstate_st + PpaData.PPAFederalState.FEDERAL_STATE_SH -> R.string.analytics_userinput_federalstate_sh + PpaData.PPAFederalState.FEDERAL_STATE_TH -> R.string.analytics_userinput_federalstate_th + PpaData.PPAFederalState.UNRECOGNIZED -> throw UnsupportedOperationException( + "PpaData.PPAFederalState.UNRECOGNIZED has no label" + ) + } + +val PpaData.PPAFederalState.federalStateShortName: String + get() = when (this) { + PpaData.PPAFederalState.FEDERAL_STATE_BW -> "BW" + PpaData.PPAFederalState.FEDERAL_STATE_BY -> "BY" + PpaData.PPAFederalState.FEDERAL_STATE_BE -> "BE" + PpaData.PPAFederalState.FEDERAL_STATE_BB -> "BB" + PpaData.PPAFederalState.FEDERAL_STATE_HB -> "HB" + PpaData.PPAFederalState.FEDERAL_STATE_HH -> "HH" + PpaData.PPAFederalState.FEDERAL_STATE_HE -> "HE" + PpaData.PPAFederalState.FEDERAL_STATE_MV -> "MV" + PpaData.PPAFederalState.FEDERAL_STATE_NI -> "NI" + PpaData.PPAFederalState.FEDERAL_STATE_NRW -> "NW" + PpaData.PPAFederalState.FEDERAL_STATE_RP -> "RP" + PpaData.PPAFederalState.FEDERAL_STATE_SL -> "SL" + PpaData.PPAFederalState.FEDERAL_STATE_SN -> "SN" + PpaData.PPAFederalState.FEDERAL_STATE_ST -> "ST" + PpaData.PPAFederalState.FEDERAL_STATE_SH -> "SH" + PpaData.PPAFederalState.FEDERAL_STATE_TH -> "TH" + PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED -> "" + PpaData.PPAFederalState.UNRECOGNIZED -> throw UnsupportedOperationException( + "PpaData.PPAFederalState.UNRECOGNIZED has no short name" + ) + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsUIModule.kt new file mode 100644 index 000000000..e7400d1ed --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsUIModule.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class AnalyticsUIModule { + + @ContributesAndroidInjector + abstract fun userInput(): AnalyticsUserInputFragment + + @Binds + @IntoMap + @CWAViewModelKey(AnalyticsUserInputViewModel::class) + abstract fun ppaUserInfoSelection( + factory: AnalyticsUserInputViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputFragment.kt new file mode 100644 index 000000000..b23dee43a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputFragment.kt @@ -0,0 +1,64 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui.input + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.AnalyticsPpaUserinfoInputFragmentBinding +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.cwaViewModelsAssisted +import javax.inject.Inject + +class AnalyticsUserInputFragment : Fragment(R.layout.analytics_ppa_userinfo_input_fragment), AutoInject { + + val navArgs by navArgs<AnalyticsUserInputFragmentArgs>() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: AnalyticsUserInputViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as AnalyticsUserInputViewModel.Factory + factory.create(navArgs.type) + } + ) + + private val binding: AnalyticsPpaUserinfoInputFragmentBinding by viewBindingLazy() + + @Inject lateinit var itemAdapter: UserInfoItemAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val toolbarLabel = when (navArgs.type) { + InputType.AGE_GROUP -> R.string.analytics_userinput_agegroup_title + InputType.FEDERAL_STATE -> R.string.analytics_userinput_federalstate_title + InputType.DISTRICT -> R.string.analytics_userinput_district_title + } + binding.toolbar.apply { + setTitle(toolbarLabel) + setNavigationOnClickListener { popBackStack() } + } + + binding.inputList.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = itemAdapter + } + vm.userInfoItems.observe2(this) { + itemAdapter.data = it + } + itemAdapter.onItemClickListener = { vm.selectUserInfoItem(it) } + vm.finishEvent.observe2(this) { popBackStack() } + } + + enum class InputType { + AGE_GROUP, + FEDERAL_STATE, + DISTRICT + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModel.kt new file mode 100644 index 000000000..7be4db1e5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModel.kt @@ -0,0 +1,130 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui.input + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.ui.day.ContactDiaryDayViewModel +import de.rki.coronawarnapp.datadonation.analytics.AnalyticsSettings +import de.rki.coronawarnapp.datadonation.analytics.common.Districts +import de.rki.coronawarnapp.datadonation.analytics.common.federalStateShortName +import de.rki.coronawarnapp.datadonation.analytics.common.labelStringRes +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.ui.toLazyString +import de.rki.coronawarnapp.util.ui.toResolvingString +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import timber.log.Timber +import java.util.Locale + +class AnalyticsUserInputViewModel @AssistedInject constructor( + @Assisted val type: AnalyticsUserInputFragment.InputType, + private val settings: AnalyticsSettings, + @AppContext private val context: Context, + private val districtsSource: Districts, + dispatcherProvider: DispatcherProvider +) : CWAViewModel() { + + private val ageGroupSource: Flow<List<UserInfoItem>> = flowOf(PpaData.PPAAgeGroup.values()) + .map { ages -> + val selected = settings.userInfoAgeGroup.value + ages.mapNotNull { age -> + if (age == PpaData.PPAAgeGroup.UNRECOGNIZED) return@mapNotNull null + UserInfoItem( + data = age, + isSelected = age == selected, + label = age.labelStringRes.toResolvingString() + ) + } + } + + private val federalStateSource: Flow<List<UserInfoItem>> = flowOf(PpaData.PPAFederalState.values()) + .map { states -> + val selected = settings.userInfoFederalState.value + val items = states + .mapNotNull { state -> + if (state == PpaData.PPAFederalState.UNRECOGNIZED) return@mapNotNull null + if (state == PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED) return@mapNotNull null + UserInfoItem( + data = state, + isSelected = state == selected, + label = state.labelStringRes.toResolvingString() + ) + } + .sortedBy { it.label.get(context).toLowerCase(Locale.ROOT) } + + val unspecified = UserInfoItem( + data = PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED, + isSelected = selected == PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED, + label = PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED.labelStringRes.toResolvingString() + ) + listOf(unspecified) + items + } + + private val districtSource: Flow<List<UserInfoItem>> = flow { emit(districtsSource.loadDistricts()) } + .map { allDistricts -> + val ourStateCode = settings.userInfoFederalState.value.federalStateShortName + allDistricts.filter { it.federalStateShortName == ourStateCode } + } + .map { districts -> + val selected = settings.userInfoDistrict.value + val items = districts + .map { district -> + UserInfoItem( + data = district, + isSelected = district.districtId == selected, + label = district.districtName.toLazyString() + ) + } + .sortedBy { it.label.get(context).toLowerCase(Locale.ROOT) } + val unspecified = UserInfoItem( + data = Districts.District(), + isSelected = 0 == selected, + label = R.string.analytics_userinput_district_unspecified.toResolvingString() + ) + listOf(unspecified) + items + } + + val userInfoItems: LiveData<List<UserInfoItem>> = when (type) { + AnalyticsUserInputFragment.InputType.AGE_GROUP -> ageGroupSource + AnalyticsUserInputFragment.InputType.FEDERAL_STATE -> federalStateSource + AnalyticsUserInputFragment.InputType.DISTRICT -> districtSource + } + .catch { Timber.e(it, "Error sourcing list items.") } + .asLiveData(context = dispatcherProvider.Default) + + val finishEvent = SingleLiveEvent<Unit>() + + fun selectUserInfoItem(item: UserInfoItem) { + when (item.data) { + is PpaData.PPAAgeGroup -> { + settings.userInfoAgeGroup.update { item.data } + } + is PpaData.PPAFederalState -> { + settings.userInfoFederalState.update { item.data } + settings.userInfoDistrict.update { 0 } + } + is Districts.District -> { + settings.userInfoDistrict.update { item.data.districtId } + } + else -> throw IllegalArgumentException() + } + finishEvent.postValue(Unit) + } + + @AssistedFactory + interface Factory : CWAViewModelFactory<ContactDiaryDayViewModel> { + fun create(type: AnalyticsUserInputFragment.InputType): AnalyticsUserInputViewModel + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItem.kt new file mode 100644 index 000000000..ead546937 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItem.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui.input + +import de.rki.coronawarnapp.util.ui.LazyString + +data class UserInfoItem( + val label: LazyString, + val isSelected: Boolean, + val data: Any +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItemAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItemAdapter.kt new file mode 100644 index 000000000..51ea3758c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/UserInfoItemAdapter.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui.input + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.AnalyticsPpaUserinfoInputAdapterItemBinding +import de.rki.coronawarnapp.ui.lists.BaseAdapter +import javax.inject.Inject + +class UserInfoItemAdapter @Inject constructor() : BaseAdapter<UserInfoItemAdapter.VH>() { + + private val internalData = mutableListOf<UserInfoItem>() + + var data: List<UserInfoItem> + get() = internalData.toList() + set(value) { + internalData.clear() + internalData.addAll(value) + notifyDataSetChanged() + } + + var onItemClickListener: (UserInfoItem) -> Unit = {} + + override fun getItemCount(): Int = internalData.size + + override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): VH = VH(parent) + + override fun onBindBaseVH(holder: VH, position: Int, payloads: MutableList<Any>) { + holder.apply { + val item = internalData[position] + bind(item) + itemView.setOnClickListener { onItemClickListener(item) } + } + } + + class VH(parent: ViewGroup) : BaseAdapter.VH(R.layout.analytics_ppa_userinfo_input_adapter_item, parent) { + private val viewBinding: AnalyticsPpaUserinfoInputAdapterItemBinding = + AnalyticsPpaUserinfoInputAdapterItemBinding.bind(itemView) + + fun bind(item: UserInfoItem) = viewBinding.apply { + label.text = item.label.get(context) + radiobutton.isChecked = item.isSelected + } + } +} 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 623b42b0b..113dd5c88 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 @@ -4,6 +4,7 @@ import dagger.Binds import dagger.Module import dagger.android.ContributesAndroidInjector import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.datadonation.analytics.ui.AnalyticsUIModule import de.rki.coronawarnapp.tracing.ui.details.TracingDetailsFragmentModule import de.rki.coronawarnapp.ui.information.InformationFragmentModule import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment @@ -28,7 +29,8 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey SettingFragmentsModule::class, SubmissionFragmentModule::class, InformationFragmentModule::class, - NewReleaseInfoFragmentModule::class + NewReleaseInfoFragmentModule::class, + AnalyticsUIModule::class ] ) abstract class MainActivityModule { diff --git a/Corona-Warn-App/src/main/res/color/radiobutton_accented.xml b/Corona-Warn-App/src/main/res/color/radiobutton_accented.xml new file mode 100644 index 000000000..d5cb9bf2e --- /dev/null +++ b/Corona-Warn-App/src/main/res/color/radiobutton_accented.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/colorAccent" android:state_checked="true" /> + <item android:color="@color/colorStableHairlineLight" /> +</selector> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_adapter_item.xml b/Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_adapter_item.xml new file mode 100644 index 000000000..6c52c3330 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_adapter_item.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?selectableItemBackground"> + + <RadioButton + android:id="@+id/radiobutton" + style="@style/Widget.AppCompat.CompoundButton.RadioButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:buttonTint="@color/radiobutton_accented" + android:buttonTintMode="src_atop" + android:clickable="false" + android:focusable="false" /> + + <TextView + android:id="@+id/label" + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="64dp" + android:layout_marginEnd="16dp" + tools:text="Keine Angabe" /> + +</FrameLayout> diff --git a/Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_fragment.xml b/Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_fragment.xml new file mode 100644 index 000000000..a729a715d --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/analytics_ppa_userinfo_input_fragment.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout 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" + android:orientation="vertical"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.Close" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/colorBackground" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:title="Ihr Alter/Bundesland/Kreis " /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/input_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/analytics_ppa_userinfo_input_adapter_item" /> + +</LinearLayout> \ 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 afb843e90..ea016ac20 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -504,10 +504,18 @@ <fragment android:id="@+id/newReleaseInfoFragment" android:name="de.rki.coronawarnapp.ui.release.NewReleaseInfoFragment" - android:label="NewReleaseInfoFragment" > + android:label="NewReleaseInfoFragment"> <argument android:name="comesFromInfoScreen" app:argType="boolean" android:defaultValue="false" /> </fragment> + <fragment + android:id="@+id/analyticsUserInputFragment" + android:name="de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment" + android:label="AnalyticsUserInputFragment"> + <argument + android:name="type" + app:argType="de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment$InputType" /> + </fragment> </navigation> 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 51fc4e047..79ddeb355 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -1610,4 +1610,60 @@ <!-- XTXT: Statistics trend stable (Accessibilty) --> <string name="statistics_trend_stable">Trend stabil</string> + <!-- #################################### + Data donation + ###################################### --> + + <!-- XHED: Analytics voluntary user input, age group toolbar title --> + <string name="analytics_userinput_agegroup_title">Ihr Alter</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_UNSPECIFIED --> + <string name="analytics_userinput_agegroup_unspecified">keine Angabe</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_0_TO_29 --> + <string name="analytics_userinput_agegroup_0_to_29">bis 30 Jahre</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_30_TO_59 --> + <string name="analytics_userinput_agegroup_30_to_59">30 bis 60</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_FROM_60 --> + <string name="analytics_userinput_agegroup_from_60">60 oder älter</string> + + <!-- XHED: Analytics voluntary user input, federal state toolbar title --> + <string name="analytics_userinput_federalstate_title">Ihr Bundesland</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_UNSPECIFIED --> + <string name="analytics_userinput_federalstate_unspecified">keine Angabe</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BW --> + <string name="analytics_userinput_federalstate_bw">Baden-Württemberg</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BY --> + <string name="analytics_userinput_federalstate_by">Bayern</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BE --> + <string name="analytics_userinput_federalstate_be">Berlin</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BB --> + <string name="analytics_userinput_federalstate_bb">Brandenburg</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_HB --> + <string name="analytics_userinput_federalstate_hb">Bremen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_HH --> + <string name="analytics_userinput_federalstate_hh">Hamburg</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_HE --> + <string name="analytics_userinput_federalstate_he">Hessen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_MV --> + <string name="analytics_userinput_federalstate_mv">Mecklenburg-Vorpommern</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_NI --> + <string name="analytics_userinput_federalstate_ni">Niedersachsen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_NRW --> + <string name="analytics_userinput_federalstate_nrw">Nordrhein-Westfalen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_RP --> + <string name="analytics_userinput_federalstate_rp">Rheinland-Pfalz</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_SL --> + <string name="analytics_userinput_federalstate_sl">Saarland</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_SN --> + <string name="analytics_userinput_federalstate_sn">Sachsen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_ST --> + <string name="analytics_userinput_federalstate_st">Sachsen-Anhalt</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_SH --> + <string name="analytics_userinput_federalstate_sh">Schleswig-Holstein</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_TH --> + <string name="analytics_userinput_federalstate_th">Thüringen</string> + + <!-- XHED: Analytics voluntary user input, district toolbar title --> + <string name="analytics_userinput_district_title">Ihr Kreis/Bezirk</string> + <!-- XTXT: Analytics voluntary user input, district: UNSPECIFIED --> + <string name="analytics_userinput_district_unspecified">keine Angabe</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 147ad3f20..46036beca 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1633,4 +1633,60 @@ <!-- XTXT: Statistics trend stable (Accessibilty) --> <string name="statistics_trend_stable">"Trend: Steady"</string> + <!-- #################################### + Data donation + ###################################### --> + + <!-- XHED: Analytics voluntary user input, age group toolbar title --> + <string name="analytics_userinput_agegroup_title">Ihr Alter</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_UNSPECIFIED --> + <string name="analytics_userinput_agegroup_unspecified">keine Angabe</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_0_TO_29 --> + <string name="analytics_userinput_agegroup_0_to_29">bis 30 Jahre</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_30_TO_59 --> + <string name="analytics_userinput_agegroup_30_to_59">30 bis 60</string> + <!-- XTXT: Analytics voluntary user input, age group: AGE_GROUP_FROM_60 --> + <string name="analytics_userinput_agegroup_from_60">60 oder älter</string> + + <!-- XHED: Analytics voluntary user input, federal state toolbar title --> + <string name="analytics_userinput_federalstate_title">Ihr Bundesland</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_UNSPECIFIED --> + <string name="analytics_userinput_federalstate_unspecified">keine Angabe</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BW --> + <string name="analytics_userinput_federalstate_bw">Baden-Württemberg</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BY --> + <string name="analytics_userinput_federalstate_by">Bayern</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BE --> + <string name="analytics_userinput_federalstate_be">Berlin</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_BB --> + <string name="analytics_userinput_federalstate_bb">Brandenburg</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_HB --> + <string name="analytics_userinput_federalstate_hb">Bremen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_HH --> + <string name="analytics_userinput_federalstate_hh">Hamburg</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_HE --> + <string name="analytics_userinput_federalstate_he">Hessen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_MV --> + <string name="analytics_userinput_federalstate_mv">Mecklenburg-Vorpommern</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_NI --> + <string name="analytics_userinput_federalstate_ni">Niedersachsen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_NRW --> + <string name="analytics_userinput_federalstate_nrw">Nordrhein-Westfalen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_RP --> + <string name="analytics_userinput_federalstate_rp">Rheinland-Pfalz</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_SL --> + <string name="analytics_userinput_federalstate_sl">Saarland</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_SN --> + <string name="analytics_userinput_federalstate_sn">Sachsen</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_ST --> + <string name="analytics_userinput_federalstate_st">Sachsen-Anhalt</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_SH --> + <string name="analytics_userinput_federalstate_sh">Schleswig-Holstein</string> + <!-- XTXT: Analytics voluntary user input, federal state: FEDERAL_STATE_TH --> + <string name="analytics_userinput_federalstate_th">Thüringen</string> + + <!-- XHED: Analytics voluntary user input, district toolbar title --> + <string name="analytics_userinput_district_title">Ihr Kreis/Bezirk</string> + <!-- XTXT: Analytics voluntary user input, district: UNSPECIFIED --> + <string name="analytics_userinput_district_unspecified">keine Angabe</string> </resources> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/DistrictsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/DistrictsTest.kt new file mode 100644 index 000000000..0bb64eb5d --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/DistrictsTest.kt @@ -0,0 +1,72 @@ +package de.rki.coronawarnapp.datadonation.analytics.common + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.google.gson.Gson +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import kotlinx.coroutines.test.runBlockingTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import testhelpers.BaseTest +import testhelpers.EmptyApplication + +@Config(sdk = [Build.VERSION_CODES.P], application = EmptyApplication::class) +@RunWith(RobolectricTestRunner::class) +class DistrictsTest : BaseTest() { + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + @After + fun tearDown() { + clearAllMocks() + } + + val context: Context + get() = ApplicationProvider.getApplicationContext() + + fun createInstance() = Districts( + context = context, + gson = Gson() + ) + + @Test + fun `loading from assets and parsing`() = runBlockingTest { + val districts = createInstance().loadDistricts() + districts.size shouldBe 412 + districts.last() shouldBe Districts.District( + districtName = "SK Weimar", + districtShortName = "WE", + districtId = 11016055, + federalStateName = "Thüringen", + federalStateShortName = "TH", + federalStateId = 13000016 + ) + } + + @Test + fun `districts have only known short names for federal states`() = runBlockingTest { + val districts = createInstance().loadDistricts() + + val stateCodesInDistricts = mutableSetOf<String>() + districts.forEach { stateCodesInDistricts.add(it.federalStateShortName) } + + val knownFederalStates = PpaData.PPAFederalState.values().filterNot { + it == PpaData.PPAFederalState.UNRECOGNIZED || it == PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED + } + + stateCodesInDistricts.size shouldBe knownFederalStates.size + + stateCodesInDistricts.sorted() shouldBe knownFederalStates.map { it.federalStateShortName }.sorted() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensionsTest.kt new file mode 100644 index 000000000..e189bf393 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/common/PpaDataExtensionsTest.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.datadonation.analytics.common + +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PpaDataExtensionsTest : BaseTest() { + + @Test + fun `federal state to short code mapping`() { + PpaData.PPAFederalState.FEDERAL_STATE_BW.federalStateShortName shouldBe "BW" + PpaData.PPAFederalState.FEDERAL_STATE_BY.federalStateShortName shouldBe "BY" + PpaData.PPAFederalState.FEDERAL_STATE_BE.federalStateShortName shouldBe "BE" + PpaData.PPAFederalState.FEDERAL_STATE_BB.federalStateShortName shouldBe "BB" + PpaData.PPAFederalState.FEDERAL_STATE_HB.federalStateShortName shouldBe "HB" + PpaData.PPAFederalState.FEDERAL_STATE_HH.federalStateShortName shouldBe "HH" + PpaData.PPAFederalState.FEDERAL_STATE_HE.federalStateShortName shouldBe "HE" + PpaData.PPAFederalState.FEDERAL_STATE_MV.federalStateShortName shouldBe "MV" + PpaData.PPAFederalState.FEDERAL_STATE_NI.federalStateShortName shouldBe "NI" + PpaData.PPAFederalState.FEDERAL_STATE_NRW.federalStateShortName shouldBe "NW" + PpaData.PPAFederalState.FEDERAL_STATE_RP.federalStateShortName shouldBe "RP" + PpaData.PPAFederalState.FEDERAL_STATE_SL.federalStateShortName shouldBe "SL" + PpaData.PPAFederalState.FEDERAL_STATE_SN.federalStateShortName shouldBe "SN" + PpaData.PPAFederalState.FEDERAL_STATE_ST.federalStateShortName shouldBe "ST" + PpaData.PPAFederalState.FEDERAL_STATE_SH.federalStateShortName shouldBe "SH" + PpaData.PPAFederalState.FEDERAL_STATE_TH.federalStateShortName shouldBe "TH" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsSettingsTest.kt new file mode 100644 index 000000000..c75702f25 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/AnalyticsSettingsTest.kt @@ -0,0 +1,79 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui + +import android.content.Context +import de.rki.coronawarnapp.datadonation.analytics.AnalyticsSettings +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.MockSharedPreferences + +class AnalyticsSettingsTest : BaseTest() { + @MockK lateinit var context: Context + lateinit var preferences: MockSharedPreferences + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + preferences = MockSharedPreferences() + every { context.getSharedPreferences("analytics_localdata", Context.MODE_PRIVATE) } returns preferences + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + fun createInstance() = AnalyticsSettings( + context = context + ) + + @Test + fun `userinfo agegroup`() { + createInstance().apply { + preferences.dataMapPeek.isEmpty() shouldBe true + + userInfoAgeGroup.value shouldBe PpaData.PPAAgeGroup.AGE_GROUP_UNSPECIFIED + userInfoAgeGroup.update { PpaData.PPAAgeGroup.AGE_GROUP_FROM_60 } + preferences.dataMapPeek["userinfo.agegroup"] shouldBe 3 + userInfoAgeGroup.value shouldBe PpaData.PPAAgeGroup.AGE_GROUP_FROM_60 + + userInfoAgeGroup.update { PpaData.PPAAgeGroup.UNRECOGNIZED } + userInfoAgeGroup.value shouldBe PpaData.PPAAgeGroup.AGE_GROUP_UNSPECIFIED + } + } + + @Test + fun `userinfo federal state`() { + createInstance().apply { + preferences.dataMapPeek.isEmpty() shouldBe true + + userInfoFederalState.value shouldBe PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED + userInfoFederalState.update { PpaData.PPAFederalState.FEDERAL_STATE_NRW } + preferences.dataMapPeek["userinfo.federalstate"] shouldBe 10 + userInfoFederalState.value shouldBe PpaData.PPAFederalState.FEDERAL_STATE_NRW + + userInfoFederalState.update { PpaData.PPAFederalState.UNRECOGNIZED } + userInfoFederalState.value shouldBe PpaData.PPAFederalState.FEDERAL_STATE_UNSPECIFIED + } + } + + @Test + fun `userinfo district`() { + createInstance().apply { + preferences.dataMapPeek.isEmpty() shouldBe true + + userInfoDistrict.value shouldBe 0 + userInfoDistrict.update { 123 } + preferences.dataMapPeek["userinfo.district"] shouldBe 123 + + userInfoDistrict.value shouldBe 123 + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModelTest.kt new file mode 100644 index 000000000..783b6020b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/ui/input/AnalyticsUserInputViewModelTest.kt @@ -0,0 +1,180 @@ +package de.rki.coronawarnapp.datadonation.analytics.ui.input + +import android.content.Context +import de.rki.coronawarnapp.datadonation.analytics.AnalyticsSettings +import de.rki.coronawarnapp.datadonation.analytics.common.Districts +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment.InputType +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData.PPAAgeGroup +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData.PPAFederalState +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.asDispatcherProvider +import testhelpers.coroutines.runBlockingTest2 +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.preferences.mockFlowPreference + +@ExtendWith(InstantExecutorExtension::class) +class AnalyticsUserInputViewModelTest : BaseTest() { + + @MockK lateinit var context: Context + @MockK lateinit var districtsSource: Districts + @MockK lateinit var analyticsSettings: AnalyticsSettings + + private val userInfoAgeGroup = mockFlowPreference(PPAAgeGroup.AGE_GROUP_UNSPECIFIED) + private val userInfoFederalState = mockFlowPreference(PPAFederalState.FEDERAL_STATE_UNSPECIFIED) + private var userInfoDistrict = mockFlowPreference(0) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { context.getString(any(), *anyVararg()) } returns "" + + every { analyticsSettings.userInfoAgeGroup } returns userInfoAgeGroup + every { analyticsSettings.userInfoFederalState } returns userInfoFederalState + every { analyticsSettings.userInfoDistrict } returns userInfoDistrict + + coEvery { districtsSource.loadDistricts() } returns emptyList() + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + fun createInstance( + inputType: InputType, + scope: CoroutineScope + ) = AnalyticsUserInputViewModel( + type = inputType, + dispatcherProvider = scope.asDispatcherProvider(), + context = context, + districtsSource = districtsSource, + settings = analyticsSettings + ) + + @Test + fun `test agegroup emission`() = runBlockingTest2(ignoreActive = true) { + userInfoAgeGroup.update { PPAAgeGroup.AGE_GROUP_0_TO_29 } + val instance = createInstance(inputType = InputType.AGE_GROUP, scope = this) + + instance.userInfoItems.observeForever { } + instance.userInfoItems.value!![0].apply { + data shouldBe PPAAgeGroup.AGE_GROUP_UNSPECIFIED + isSelected shouldBe false + } + instance.userInfoItems.value!![1].apply { + data shouldBe PPAAgeGroup.AGE_GROUP_0_TO_29 + isSelected shouldBe true + } + instance.userInfoItems.value!![3].apply { + data shouldBe PPAAgeGroup.AGE_GROUP_FROM_60 + isSelected shouldBe false + } + } + + @Test + fun `test agegroup selection`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(inputType = InputType.AGE_GROUP, scope = this) + instance.finishEvent.value shouldBe null + + instance.selectUserInfoItem( + UserInfoItem(data = PPAAgeGroup.AGE_GROUP_30_TO_59, label = mockk(), isSelected = false) + ) + + userInfoAgeGroup.value shouldBe PPAAgeGroup.AGE_GROUP_30_TO_59 + userInfoFederalState.value shouldBe PPAFederalState.FEDERAL_STATE_UNSPECIFIED + userInfoDistrict.value shouldBe 0 + + instance.finishEvent.value shouldBe Unit + } + + @Test + fun `test federal state emission`() = runBlockingTest2(ignoreActive = true) { + userInfoFederalState.update { PPAFederalState.FEDERAL_STATE_HH } + val instance = createInstance(inputType = InputType.FEDERAL_STATE, scope = this) + + instance.userInfoItems.observeForever { } + instance.userInfoItems.value!![0].apply { + data shouldBe PPAFederalState.FEDERAL_STATE_UNSPECIFIED + isSelected shouldBe false + } + instance.userInfoItems.value!![1].apply { + data shouldBe PPAFederalState.FEDERAL_STATE_BW + isSelected shouldBe false + } + instance.userInfoItems.value!![6].apply { + data shouldBe PPAFederalState.FEDERAL_STATE_HH + isSelected shouldBe true + } + instance.userInfoItems.value!![16].apply { + data shouldBe PPAFederalState.FEDERAL_STATE_TH + isSelected shouldBe false + } + } + + @Test + fun `test federal state selection`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(inputType = InputType.FEDERAL_STATE, scope = this) + + instance.finishEvent.value shouldBe null + userInfoDistrict.update { 12345 } // Because federal state selection should reset this + + instance.selectUserInfoItem( + UserInfoItem(data = PPAFederalState.FEDERAL_STATE_NRW, label = mockk(), isSelected = false) + ) + + userInfoAgeGroup.value shouldBe PPAAgeGroup.AGE_GROUP_UNSPECIFIED + userInfoFederalState.value shouldBe PPAFederalState.FEDERAL_STATE_NRW + userInfoDistrict.value shouldBe 0 + + instance.finishEvent.value shouldBe Unit + } + + @Test + fun `test district emission`() = runBlockingTest2(ignoreActive = true) { + userInfoFederalState.update { PPAFederalState.FEDERAL_STATE_NRW } + + val ourDistrict = Districts.District( + districtId = 1234, + federalStateShortName = "NW" + ) + val notOurDistrict = Districts.District( + districtId = 5678, + federalStateShortName = "BE" + ) + coEvery { districtsSource.loadDistricts() } returns listOf(ourDistrict, notOurDistrict) + + val instance = createInstance(inputType = InputType.DISTRICT, scope = this) + + instance.userInfoItems.observeForever { } + instance.userInfoItems.value!![0].data shouldBe Districts.District() + instance.userInfoItems.value!![1].data shouldBe ourDistrict + } + + @Test + fun `test district selection`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(inputType = InputType.DISTRICT, scope = this) + instance.finishEvent.value shouldBe null + + instance.selectUserInfoItem( + UserInfoItem(data = Districts.District(districtId = 9000), label = mockk(), isSelected = false) + ) + userInfoAgeGroup.value shouldBe PPAAgeGroup.AGE_GROUP_UNSPECIFIED + userInfoFederalState.value shouldBe PPAFederalState.FEDERAL_STATE_UNSPECIFIED + userInfoDistrict.value shouldBe 9000 + + instance.finishEvent.value shouldBe Unit + } +} -- GitLab