Skip to content

Commit e9b12f4

Browse files
committed
Merge branch 'v1.9.0-beta02-release'
* v1.9.0-beta02-release: Finish releasing v1.9.0-beta02 Releasing v1.9.0-beta02 Close dialogs even if the managing view is never attached.
2 parents 1483fe2 + 9d6e91b commit e9b12f4

File tree

5 files changed

+62
-11
lines changed

5 files changed

+62
-11
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android.useAndroidX=true
88
systemProp.org.gradle.internal.publish.checksums.insecure=true
99

1010
GROUP=com.squareup.workflow1
11-
VERSION_NAME=1.9.0-beta02-SNAPSHOT
11+
VERSION_NAME=1.9.0-beta03-SNAPSHOT
1212

1313
POM_DESCRIPTION=Square Workflow
1414

workflow-ui/core-android/api/core-android.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ public final class com/squareup/workflow1/ui/WorkflowViewStub : android/view/Vie
269269

270270
public final class com/squareup/workflow1/ui/androidx/WorkflowAndroidXSupport {
271271
public static final field INSTANCE Lcom/squareup/workflow1/ui/androidx/WorkflowAndroidXSupport;
272+
public final fun lifecycleOwnerFromContext (Landroid/content/Context;)Landroidx/lifecycle/LifecycleOwner;
272273
public final fun lifecycleOwnerFromViewTreeOrContextOrNull (Landroid/view/View;)Landroidx/lifecycle/LifecycleOwner;
273274
public final fun stateRegistryOwnerFromViewTreeOrContext (Landroid/view/View;)Landroidx/savedstate/SavedStateRegistryOwner;
274275
}

workflow-ui/core-android/src/androidTest/java/com/squareup/workflow1/ui/container/DialogIntegrationTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Dialog
44
import android.view.View
55
import android.widget.EditText
66
import androidx.activity.ComponentActivity
7+
import androidx.lifecycle.Lifecycle.State.DESTROYED
78
import androidx.test.ext.junit.rules.ActivityScenarioRule
89
import androidx.test.ext.junit.runners.AndroidJUnit4
910
import com.google.common.truth.Truth.assertThat
@@ -133,4 +134,19 @@ internal class DialogIntegrationTest {
133134
assertThat(latestDialog).isSameInstanceAs(originalDialogOne)
134135
}
135136
}
137+
138+
@Test fun finishingActivityEarlyDismissesDialogs() {
139+
val screen = BodyAndOverlaysScreen(
140+
ContentRendering("body"),
141+
DialogRendering("dialog", ContentRendering("content"))
142+
)
143+
144+
scenario.onActivity { activity ->
145+
val root = WorkflowLayout(activity)
146+
root.show(screen)
147+
}
148+
149+
scenario.moveToState(DESTROYED)
150+
assertThat(latestDialog?.isShowing).isFalse()
151+
}
136152
}

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/androidx/WorkflowAndroidXSupport.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ import kotlin.reflect.cast
1515
* Namespace for some helper functions for interacting with the AndroidX libraries.
1616
*/
1717
public object WorkflowAndroidXSupport {
18+
/**
19+
* Returns the [LifecycleOwner] managing [context].
20+
*
21+
* @throws IllegalArgumentException if [context] is unmanaged
22+
*/
23+
@WorkflowUiExperimentalApi
24+
public fun lifecycleOwnerFromContext(context: Context): LifecycleOwner =
25+
requireNotNull(context.ownerOrNull(LifecycleOwner::class)) {
26+
"Expected $context to lead to a LifecycleOwner"
27+
}
28+
1829
/**
1930
* Tries to get the parent lifecycle from the current view via [ViewTreeLifecycleOwner], if that
2031
* fails it looks up the context chain for a [LifecycleOwner], and if that fails it just returns

workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogSessions.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import android.view.MotionEvent
99
import android.view.View
1010
import android.view.View.OnAttachStateChangeListener
1111
import android.view.ViewTreeObserver.OnGlobalLayoutListener
12+
import androidx.core.view.doOnAttach
13+
import androidx.lifecycle.DefaultLifecycleObserver
1214
import androidx.lifecycle.LifecycleOwner
1315
import androidx.lifecycle.ViewTreeLifecycleOwner
1416
import com.squareup.workflow1.ui.Compatible
1517
import com.squareup.workflow1.ui.ViewEnvironment
1618
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
1719
import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport
20+
import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport.lifecycleOwnerFromContext
1821
import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner
1922
import com.squareup.workflow1.ui.androidx.WorkflowSavedStateRegistryAggregator
2023
import com.squareup.workflow1.ui.container.DialogSession.KeyAndBundle
@@ -277,11 +280,11 @@ public class LayeredDialogSessions private constructor(
277280
): LayeredDialogSessions {
278281
val boundsRect = Rect()
279282
if (view.isAttachedToWindow) view.getGlobalVisibleRect(boundsRect)
280-
val bounds = MutableStateFlow(Rect(boundsRect))
283+
val boundsStateFlow = MutableStateFlow(Rect(boundsRect))
281284

282285
return LayeredDialogSessions(
283286
context = view.context,
284-
bounds = bounds,
287+
bounds = boundsStateFlow,
285288
cancelEvents = {
286289
// Note similar code in DialogSession.
287290

@@ -298,16 +301,36 @@ public class LayeredDialogSessions private constructor(
298301
"Expected a ViewTreeLifecycleOwner on $view"
299302
}
300303
}.also { dialogs ->
304+
fun closeAll() {
305+
dialogs.update(emptyList(), ViewEnvironment.EMPTY) {}
306+
}
301307

302-
val boundsListener = OnGlobalLayoutListener {
303-
if (view.getGlobalVisibleRect(boundsRect) && boundsRect != bounds.value) {
304-
bounds.value = Rect(boundsRect)
305-
}
306-
// Should we close the dialogs if getGlobalVisibleRect returns false?
307-
// https://github.com/square/workflow-kotlin/issues/599
308+
// We rely on the hosting View's WorkflowLifecycleOwner to tell us to tear things down.
309+
// WorkflowLifecycleOwner gets hooked up when the View is attached to its window.
310+
// But the Activity might finish before the hosting view is ever attached. And we have
311+
// lots of time to show Dialogs before then. They will leak.
312+
//
313+
// To guard against that we hang a default observer directly off of the Activity that
314+
// will close all Dialogs when it is destroyed; and we remove it as soon as the hosting
315+
// view is attached for the first time.
316+
val failsafe = object : DefaultLifecycleObserver {
317+
override fun onDestroy(owner: LifecycleOwner) = closeAll()
318+
}
319+
lifecycleOwnerFromContext(view.context).lifecycle.addObserver(failsafe)
320+
view.doOnAttach {
321+
lifecycleOwnerFromContext(it.context).lifecycle.removeObserver(failsafe)
308322
}
309323

324+
// While the hosting view is attached, monitor its bounds and report them
325+
// through boundsStateFlow so that managed Dialogs can constrain themselves
326+
// accordingly.
310327
val attachStateChangeListener = object : OnAttachStateChangeListener {
328+
val boundsListener = OnGlobalLayoutListener {
329+
if (view.getGlobalVisibleRect(boundsRect) && boundsRect != boundsStateFlow.value) {
330+
boundsStateFlow.value = Rect(boundsRect)
331+
}
332+
}
333+
311334
override fun onViewAttachedToWindow(v: View) {
312335
boundsListener.onGlobalLayout()
313336
v.viewTreeObserver.addOnGlobalLayoutListener(boundsListener)
@@ -316,9 +339,9 @@ public class LayeredDialogSessions private constructor(
316339
override fun onViewDetachedFromWindow(v: View) {
317340
// Don't leak the dialogs if we're suddenly yanked out of view.
318341
// https://github.com/square/workflow-kotlin/issues/314
319-
dialogs.update(emptyList(), ViewEnvironment.EMPTY) {}
342+
closeAll()
320343
v.viewTreeObserver.removeOnGlobalLayoutListener(boundsListener)
321-
bounds.value = Rect()
344+
boundsStateFlow.value = Rect()
322345
}
323346
}
324347

0 commit comments

Comments
 (0)