From 97e4b27d918e91ca58659f7d27fe70f79b479077 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Tue, 23 Feb 2021 14:15:01 +0100
Subject: [PATCH] Fix comment field not saving on app moving into the
 background (EXPOSUREAPP-5108) #2424

* Generalize the lifecycle callback extension on views and extract it.

* Use `addOnWindowFocusChangeListener` to detect when the user puts the app into the background.

Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com>
---
 .../tabs/common/DiaryCircumstancesTextView.kt |  4 ++
 .../util/lists/modular/mods/SavedStateMod.kt  | 38 +++++-----------
 .../coronawarnapp/util/ui/ViewExtensions.kt   | 44 +++++++++++++++++++
 3 files changed, 59 insertions(+), 27 deletions(-)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt
index 11db3cab1..012c1a42d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt
@@ -36,6 +36,10 @@ class DiaryCircumstancesTextView @JvmOverloads constructor(
             }
             imeOptions = EditorInfo.IME_ACTION_DONE
             setRawInputType(InputType.TYPE_CLASS_TEXT)
+            // When the user entered something and puts the app into the background
+            viewTreeObserver.addOnWindowFocusChangeListener {
+                notifyTextChanged(text.toString())
+            }
         }
         infoButton = findViewById(R.id.info_button)
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/lists/modular/mods/SavedStateMod.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/lists/modular/mods/SavedStateMod.kt
index 9356a9ad7..a102f8890 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/lists/modular/mods/SavedStateMod.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/lists/modular/mods/SavedStateMod.kt
@@ -2,13 +2,10 @@ package de.rki.coronawarnapp.util.lists.modular.mods
 
 import android.os.Parcelable
 import androidx.annotation.Keep
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.findFragment
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
 import androidx.recyclerview.widget.RecyclerView
 import de.rki.coronawarnapp.util.lists.modular.ModularAdapter
+import de.rki.coronawarnapp.util.ui.addLifecycleEventCallback
 import timber.log.Timber
 
 @Keep
@@ -27,38 +24,25 @@ class SavedStateMod<T : ModularAdapter.VH> :
     }
 
     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
-        val hostLifecycle = recyclerView.hostLifecycle ?: return
-
-        val observer = object : LifecycleObserver {
-            @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
-            fun onStop() {
-                savedStates.clear()
-
-                getAllViewHolders(recyclerView).filterIsInstance<StateSavingVH>().forEach { vh ->
-                    val key = vh.savedStateKey
-                    val state = vh.onSaveState()
-                    if (key != null && state != null) {
-                        savedStates[key] = state
-                    }
+        recyclerView.addLifecycleEventCallback(type = Lifecycle.Event.ON_STOP) {
+            savedStates.clear()
+
+            getAllViewHolders(recyclerView).filterIsInstance<StateSavingVH>().forEach { vh ->
+                val key = vh.savedStateKey
+                val state = vh.onSaveState()
+                if (key != null && state != null) {
+                    savedStates[key] = state
                 }
-
-                hostLifecycle.removeObserver(this)
             }
+
+            true
         }
-        hostLifecycle.addObserver(observer)
     }
 
     override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
         // NOOP
     }
 
-    private val RecyclerView.hostLifecycle: Lifecycle?
-        get() = try {
-            findFragment<Fragment>().viewLifecycleOwner.lifecycle
-        } catch (e: Exception) {
-            null
-        }
-
     private fun getAllViewHolders(recyclerView: RecyclerView): List<RecyclerView.ViewHolder> = try {
         (0..recyclerView.childCount)
             .mapNotNull { recyclerView.getChildAt(it) }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewExtensions.kt
index 285a6d9a1..9acd45a08 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/ViewExtensions.kt
@@ -2,7 +2,14 @@ package de.rki.coronawarnapp.util.ui
 
 import android.view.View
 import androidx.databinding.BindingAdapter
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.findFragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.OnLifecycleEvent
 import de.rki.coronawarnapp.util.recyclerview.ThrottledClickListener
+import timber.log.Timber
 
 @BindingAdapter("gone")
 fun View.setGone(gone: Boolean) {
@@ -16,3 +23,40 @@ fun View.setInvisible(invisible: Boolean) {
 
 fun View.setOnClickListenerThrottled(interval: Long = 300L, listenerBlock: (View) -> Unit) =
     setOnClickListener(ThrottledClickListener(interval, listenerBlock))
+
+val View.hostLifecycle: Lifecycle?
+    get() = try {
+        findFragment<Fragment>().viewLifecycleOwner.lifecycle
+    } catch (e: Exception) {
+        Timber.v("Couldn't find viewLifecycleOwner for %s", this)
+        null
+    }
+
+/**
+ * Allows your view to receive callbacks from the host Fragment's lifecycle
+ * Your callback is invoked when the owning Fragment/Activity receives the specified event state.
+ *
+ *  @param callback returns true if it should be consumed (one-time callback), or false if it was to stay registered.
+ *
+ * @return true if the callback has been added. Otherwise returns false,
+ * i.e. if the view doesn't have a viewLifecycleOwner due to not being attached.
+ */
+fun View.addLifecycleEventCallback(
+    type: Lifecycle.Event,
+    callback: () -> Boolean
+): Boolean {
+    val hostLifecycle = hostLifecycle ?: return false
+
+    val observer = object : LifecycleObserver {
+        @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
+        fun onLifecycleEvent(owner: LifecycleOwner, event: Lifecycle.Event) {
+            if (event != type) return
+            Timber.v("%s triggered %s for %s", owner, event, this@addLifecycleEventCallback)
+            val consumed = callback()
+            if (consumed) hostLifecycle.removeObserver(this)
+        }
+    }
+
+    hostLifecycle.addObserver(observer)
+    return true
+}
-- 
GitLab