From 8b4b7897ad16348a0fc9f30c510f51c9ec30bed4 Mon Sep 17 00:00:00 2001
From: BMItter <46747780+BMItter@users.noreply.github.com>
Date: Wed, 25 Nov 2020 14:35:53 +0100
Subject: [PATCH] Exposurewindows adjustments & share, Testmenu
 (EXPOSUREAPP-3898) (#1710)

* preparation share exposureWindows via email etc. - wip

* Exposure Windows can be shared if data is availabe

* Manifest and provider path deviceForTesters only

* fixed strange conflicts

* klint

* Pretty exposure window export

* removed testlog

* removed relicts
---
 .../src/deviceForTesters/AndroidManifest.xml  | 21 ++++++++
 .../risklevel/entities/ExposureWindowJson.kt  | 19 +++++++
 .../risklevel/entities/ScanInstanceJson.kt    | 15 ++++++
 .../ui/TestRiskLevelCalculationFragment.kt    | 51 ++++++++++++-------
 ...iskLevelCalculationFragmentCWAViewModel.kt | 42 ++++++++++++---
 .../fragment_test_risk_level_calculation.xml  | 19 ++++---
 .../res/xml/provider_paths.xml                |  4 ++
 7 files changed, 137 insertions(+), 34 deletions(-)
 create mode 100644 Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml
 create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt
 create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt
 create mode 100644 Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml

diff --git a/Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml b/Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml
new file mode 100644
index 000000000..ca1b20da3
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="LockedOrientationActivity"
+    package="de.rki.coronawarnapp">
+
+    <application>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.fileProvider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/provider_paths"/>
+        </provider>
+
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt
new file mode 100644
index 000000000..ad35b6454
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt
@@ -0,0 +1,19 @@
+package de.rki.coronawarnapp.test.risklevel.entities
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+
+data class ExposureWindowJson(
+    val dateMillisSinceEpoch: Long,
+    val reportType: Int,
+    val infectiousness: Int,
+    val calibrationConfidence: Int,
+    val scanInstances: List<ScanInstanceJson>
+)
+
+fun ExposureWindow.toExposureWindowJson(): ExposureWindowJson = ExposureWindowJson(
+    dateMillisSinceEpoch = dateMillisSinceEpoch,
+    reportType = reportType,
+    infectiousness = infectiousness,
+    calibrationConfidence = calibrationConfidence,
+    scanInstances = scanInstances.map { it.toScanInstanceJson() }
+)
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt
new file mode 100644
index 000000000..0b42a6bc7
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt
@@ -0,0 +1,15 @@
+package de.rki.coronawarnapp.test.risklevel.entities
+
+import com.google.android.gms.nearby.exposurenotification.ScanInstance
+
+data class ScanInstanceJson(
+    val typicalAttenuationDb: Int,
+    val minAttenuationDb: Int,
+    val secondsSinceLastScan: Int
+)
+
+fun ScanInstance.toScanInstanceJson(): ScanInstanceJson = ScanInstanceJson(
+    typicalAttenuationDb = typicalAttenuationDb,
+    minAttenuationDb = minAttenuationDb,
+    secondsSinceLastScan = secondsSinceLastScan
+)
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt
index 7087a044d..e3b014dea 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt
@@ -1,10 +1,13 @@
 package de.rki.coronawarnapp.test.risklevel.ui
 
+import android.content.Intent
 import android.os.Bundle
 import android.view.View
 import android.widget.RadioButton
 import android.widget.RadioGroup
 import android.widget.Toast
+import androidx.core.app.ShareCompat
+import androidx.core.content.FileProvider
 import androidx.core.view.ViewCompat
 import androidx.core.view.children
 import androidx.fragment.app.Fragment
@@ -20,6 +23,8 @@ import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
+import timber.log.Timber
+import java.io.File
 import javax.inject.Inject
 
 @Suppress("LongMethod")
@@ -42,49 +47,43 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-
         vm.tracingCardState.observe2(this) {
             binding.tracingCard = it
         }
-
         binding.settingsViewModel = settingsViewModel
-
         vm.showRiskStatusCard.observe2(this) {
             binding.showRiskStatusCard = it
         }
-
         binding.buttonRetrieveDiagnosisKeys.setOnClickListener { vm.retrieveDiagnosisKeys() }
         binding.buttonCalculateRiskLevel.setOnClickListener { vm.calculateRiskLevel() }
         binding.buttonClearDiagnosisKeyCache.setOnClickListener { vm.clearKeyCache() }
-
         binding.buttonResetRiskLevel.setOnClickListener { vm.resetRiskLevel() }
+        binding.buttonExposureWindowsShare.setOnClickListener { vm.shareExposureWindows() }
         vm.riskLevelResetEvent.observe2(this) {
             Toast.makeText(
                 requireContext(), "Reset done, please fetch diagnosis keys from server again",
                 Toast.LENGTH_SHORT
             ).show()
         }
-
         vm.additionalRiskCalcInfo.observe2(this) {
             binding.labelRiskAdditionalInfo.text = it
         }
-
         vm.aggregatedRiskResult.observe2(this) {
             binding.labelAggregatedRiskResult.text = it
         }
-
-        vm.exposureWindowCountString.observe2(this) {
-            binding.labelExposureWindowCount.text = it
-        }
-
-        vm.exposureWindows.observe2(this) {
-            binding.labelExposureWindows.text = it
-        }
-
         vm.backendParameters.observe2(this) {
             binding.labelBackendParameters.text = it
         }
-
+        vm.exposureWindowCount.observe2(this) { exposureWindowCount ->
+            binding.labelExposureWindowCount.text = "Retrieved $exposureWindowCount Exposure Windows"
+            binding.buttonExposureWindowsShare.visibility = when (exposureWindowCount > 0) {
+                true -> View.VISIBLE
+                false -> View.GONE
+            }
+        }
+        vm.shareFileEvent.observe2(this) {
+            shareExposureWindowsFile(it)
+        }
         vm.fakeWindowsState.observe2(this) { currentType ->
             binding.apply {
                 if (fakeWindowsToggleGroup.childCount != TestSettings.FakeExposureWindowTypes.values().size) {
@@ -101,7 +100,6 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le
                         }
                     }
                 }
-
                 fakeWindowsToggleGroup.children.forEach {
                     it as RadioButton
                     it.isChecked = it.text == currentType.name
@@ -117,8 +115,23 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le
         }
     }
 
+    private fun shareExposureWindowsFile(file: File) {
+        Timber.d("Opening Share-Intent for Exposure Windows")
+        val shareFileUri =
+            FileProvider.getUriForFile(requireContext(), requireContext().packageName + ".fileProvider", file)
+        val shareIntent = ShareCompat.IntentBuilder
+            .from(requireActivity())
+            .setStream(shareFileUri)
+            .setType("text/plain")
+            .intent
+            .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+        if (shareIntent.resolveActivity(requireActivity().packageManager) != null) {
+            startActivity(shareIntent)
+        }
+    }
+
     companion object {
-        val TAG: String = TestRiskLevelCalculationFragment::class.simpleName!!
+        private val TAG = TestRiskLevelCalculationFragment::class.java.simpleName
         val MENU_ITEM = TestMenuItem(
             title = "ENF v2 Calculation",
             description = "Window Mode related overview.",
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
index 8cbf5d39e..63afe0209 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
@@ -3,6 +3,8 @@ package de.rki.coronawarnapp.test.risklevel.ui
 import android.content.Context
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.asLiveData
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
@@ -34,11 +36,13 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.sample
 import kotlinx.coroutines.withContext
 import org.joda.time.Instant
 import timber.log.Timber
+import java.io.File
 import java.util.Date
 import java.util.concurrent.TimeUnit
 
@@ -46,7 +50,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
     @Assisted private val handle: SavedStateHandle,
     @Assisted private val exampleArg: String?,
     @AppContext private val context: Context, // App context
-    dispatcherProvider: DispatcherProvider,
+    private val dispatcherProvider: DispatcherProvider,
     private val taskController: TaskController,
     private val keyCacheRepository: KeyCacheRepository,
     private val appConfigProvider: AppConfigProvider,
@@ -57,6 +61,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
     dispatcherProvider = dispatcherProvider
 ) {
 
+    // Use unique instance for pretty output
+    private val gson: Gson by lazy {
+        GsonBuilder().setPrettyPrinting().create()
+    }
+
     val fakeWindowsState = testSettings.fakeExposureWindows.flow.asLiveData()
 
     init {
@@ -66,6 +75,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
     }
 
     val riskLevelResetEvent = SingleLiveEvent<Unit>()
+    val shareFileEvent = SingleLiveEvent<File>()
 
     val showRiskStatusCard = SubmissionRepository.deviceUIStateFlow.map {
         it.withSuccess(false) { true }
@@ -75,14 +85,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
         .sample(150L)
         .asLiveData(dispatcherProvider.Default)
 
-    val exposureWindowCountString = riskLevelStorage
-        .exposureWindows
-        .map { "Retrieved ${it.size} Exposure Windows" }
-        .asLiveData()
-
-    val exposureWindows = riskLevelStorage
+    val exposureWindowCount = riskLevelStorage
         .exposureWindows
-        .map { if (it.isEmpty()) "Exposure windows list is empty" else it.toString() }
+        .map { it.size }
         .asLiveData()
 
     val aggregatedRiskResult = riskLevelStorage
@@ -216,6 +221,27 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
         }
     }
 
+    fun shareExposureWindows() {
+        Timber.d("Creating text file for Exposure Windows")
+        launch(dispatcherProvider.IO) {
+            val exposureWindows = riskLevelStorage.exposureWindows.firstOrNull()
+
+            val path = File(context.cacheDir, "share/")
+            path.mkdirs()
+
+            val file = File(path, "exposureWindows.txt")
+            file.bufferedWriter()
+                .use {
+                    if (exposureWindows.isNullOrEmpty()) {
+                        it.appendLine("Exposure windows list was empty")
+                    } else {
+                        it.appendLine(gson.toJson(exposureWindows))
+                    }
+                }
+            shareFileEvent.postValue(file)
+        }
+    }
+
     fun clearKeyCache() {
         Timber.d("Clearing key cache")
         launch { keyCacheRepository.clear() }
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml
index 0ea69b5c0..1e6d0bf1b 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml
@@ -23,6 +23,7 @@
             type="de.rki.coronawarnapp.ui.tracing.card.TracingCardState" />
     </data>
 
+
     <ScrollView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -33,7 +34,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical">
-
+            
             <LinearLayout
                 android:id="@+id/environment_container"
                 style="@style/card"
@@ -170,13 +171,17 @@
                 android:layout_height="wrap_content"
                 android:text="-" />
 
-            <TextView
-                android:id="@+id/label_exposure_windows"
-                android:layout_width="wrap_content"
+            <Button
+                android:id="@+id/buttonExposureWindowsShare"
+                style="@style/buttonPrimary"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:paddingTop="5dp"
-                android:text="-" />
+                android:layout_marginTop="@dimen/spacing_normal"
+                android:layout_marginBottom="@dimen/spacing_normal"
+                android:text="Share ExposureWindows" />
 
         </LinearLayout>
     </ScrollView>
-</layout>
\ No newline at end of file
+
+
+</layout>
diff --git a/Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml b/Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml
new file mode 100644
index 000000000..b6522dfad
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <cache-path name="share" path="share/" />
+</paths>
\ No newline at end of file
-- 
GitLab