Skip to content

In-app message leaks the hosting Activity #18

Description

@WojciechPawlica

SDK / artifact

com.synerise.sdk:synerise-mobile-sdk

Confirmed in 6.13.0 and 6.14.0 (sources jar).

Summary

InAppRenderingManager is a process-wide singleton (held via the static instance field). Its private Dialog dialog field is assigned exactly once, when the dialog is created, and is NEVER set back to null. Every dismissal path only calls dialog.cancel()/dismiss(); none clears the field. As a result the singleton keeps holding the dismissed Dialog, and through Dialog.mContext it keeps holding the Activity the dialog was created with — until the next in-app overwrites the field. When that Activity is destroyed before the next in-app, it leaks.

Source evidence (InAppRenderingManager.java)

  • Field declaration:
private Dialog dialog;                                   // ~line 87
  • The ONLY assignment (inside show(...)):
dialog = new Dialog(currentActivity, ...);               // ~line 128
  • Dismissal paths cancel but never null the field:
    • OnDismissListener.onDismiss(...) (~line 176-184)
    • dismissInAppDialog() (~line 753-777)
    • performInAppClose(...) -> dismissInAppDialog() (~line 742-751)

In all of them: dialog.cancel() is called, but dialog = null is never set.

(grep for "dialog =" returns only the declaration-time new Dialog(...) and a dialog == null null-check; there is no clearing assignment anywhere.)

Reproduction

  1. Trigger an in-app message while some Activity is in the foreground.
  2. Dismiss the in-app (tap close, JS close/hide, or it is dismissed when leaving) and finish/destroy that Activity.
  3. Observe with LeakCanary.

LeakCanary trace (abridged)

GC Root: Global variable in native code
...
static InAppRenderingManager.instance
 -> InAppRenderingManager.dialog
     -> Dialog.mContext (android.view.ContextThemeWrapper)
         wrapping activity <HostActivity> with mDestroyed = true
         Dialog#mDecor is null            // i.e. the dialog was dismissed
 -> ContextWrapper.mBase
 => <HostActivity> instance (LEAKING)

Dialog#mDecor is null confirms the dialog was already dismissed; the only thing still retaining the destroyed Activity is the un-cleared InAppRenderingManager.dialog field.

Impact

  • Every dismissed in-app leaves a dangling reference to its host Activity.
  • The reference becomes a real Activity leak whenever that Activity is destroyed before the next in-app is shown (e.g. user closes the screen after seeing an in-app).
  • Retains the whole Activity view tree + WebView.

Requested fix

Null the dialog field after it is dismissed. The natural place is the OnDismissListener.onDismiss() callback (and/or dismissInAppDialog()), e.g.:

dialog.setOnDismissListener(d -> {
    isCampaignCurrentlyShown = false;
    currentInAppCampaignHash = null;
    inAppListener.onDismissed(inAppMessageData);
    dialog = null;                 // <-- release the Dialog (and its Activity context)
});

Please also review related per-dialog fields captured by the singleton/handlers (e.g. viewCreatingHandler, the WebView) so the whole view tree is released on dismissal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions