Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
boolean handled = false;
if (pendingAction != null) {
handled = executeAction(context, pendingAction);
pendingAction = null;
// Only clear pending action if it was handled.
// This allows the action to be processed later when SDK is fully initialized
// (e.g., when customActionHandler becomes available after initialize() is called).
if (handled) {
pendingAction = null;
Copy link
Copy Markdown
Contributor

@franco-zalamena-iterable franco-zalamena-iterable Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if it does not manage to execute the action? Can't this pendingAction become residual?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've updated the code to clear any previous unhandled pending action when a new push action comes in. This prevents residual actions from accumulating if they were never handled. The logic now clears pendingAction at the start of handlePushAction() before setting up the new action.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as stated in the other comment, that should be defined by code here, if we should have a pendingAction after not being able to handle it that is fine, but i feel like if this was not handled here it won't be in the next time if nothing changes, so we can just be calling processPendingAction indefinitely with nothing changing

}
}
return handled;
}
Expand All @@ -38,6 +43,13 @@
IterableLogger.e(TAG, "handlePushAction: extras == null, can't handle push action");
return;
}

// Store the application context if not already set, so custom actions can be
// processed even when the SDK hasn't been fully initialized (e.g., openApp=false)
if (IterableApi.sharedInstance._applicationContext == null && context != null) {
IterableApi.sharedInstance._applicationContext = context.getApplicationContext();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a method for initializing it so we don't call _applicationContext directly, if something else is necessary for pushes to work we can add it there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I've created a new initializeForPush(Context context) method in IterableApi that encapsulates the context initialization. This method:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Only sets the context if it hasn't been set already
  • Includes proper null checking and logging
  • Makes it easy to add additional push-related initialization in the future

}

IterableNotificationData notificationData = new IterableNotificationData(intent.getExtras());
String actionIdentifier = intent.getStringExtra(IterableConstants.ITERABLE_DATA_ACTION_IDENTIFIER);
IterableAction action = null;
Expand Down Expand Up @@ -87,7 +99,7 @@
Intent launcherIntent = IterableNotificationHelper.getMainActivityIntent(context);
launcherIntent.putExtras(intent.getExtras());
launcherIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (launcherIntent.resolveActivity(context.getPackageManager()) != null) {

Check warning

Code scanning / CodeQL

Dereferenced variable may be null Warning

Variable
context
may be null at this access as suggested by
this
null guard.
context.startActivity(launcherIntent);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,59 @@ public void testLegacyDeepLinkPayload() throws Exception {
assertEquals("https://example.com", capturedAction.getValue().getData());
}

@Test
public void testBackgroundCustomActionWithNonInitializedSDK() throws Exception {
// Reset to simulate SDK not being initialized
IterableTestUtils.resetIterableApi();

// Verify context is initially null
assertNull(IterableApi.sharedInstance._applicationContext);

IterablePushActionReceiver iterablePushActionReceiver = new IterablePushActionReceiver();
Intent intent = new Intent(IterableConstants.ACTION_PUSH_ACTION);
intent.putExtra(IterableConstants.ITERABLE_DATA_ACTION_IDENTIFIER, "remindMeButton");
intent.putExtra(IterableConstants.ITERABLE_DATA_KEY, IterableTestUtils.getResourceString("push_payload_background_custom_action.json"));

// Receive push action when SDK is not initialized
iterablePushActionReceiver.onReceive(ApplicationProvider.getApplicationContext(), intent);

// Verify that context was stored even without SDK initialization
assertNotNull(IterableApi.sharedInstance._applicationContext);

// Verify that the main app activity was NOT launched (openApp=false)
Application application = ApplicationProvider.getApplicationContext();
Intent activityIntent = shadowOf(application).peekNextStartedActivity();
assertNull(activityIntent);
}

@Test
public void testBackgroundCustomActionProcessedAfterSDKInit() throws Exception {
// Reset to simulate SDK not being initialized
IterableTestUtils.resetIterableApi();

IterablePushActionReceiver iterablePushActionReceiver = new IterablePushActionReceiver();
Intent intent = new Intent(IterableConstants.ACTION_PUSH_ACTION);
intent.putExtra(IterableConstants.ITERABLE_DATA_ACTION_IDENTIFIER, "remindMeButton");
intent.putExtra(IterableConstants.ITERABLE_DATA_KEY, IterableTestUtils.getResourceString("push_payload_background_custom_action.json"));

// Receive push action when SDK is not initialized (action won't be handled)
iterablePushActionReceiver.onReceive(ApplicationProvider.getApplicationContext(), intent);

// Now initialize SDK with a custom action handler
stubAnyRequestReturningStatusCode(server, 200, "{}");
final boolean[] handlerCalled = {false};
IterableTestUtils.createIterableApiNew(builder ->
builder.setCustomActionHandler((action, actionContext) -> {
handlerCalled[0] = true;
assertEquals("snoozeReminder", action.getType());
return true;
})
);

// Verify that the custom action handler was called during initialization
// (processPendingAction is called in initialize())
assertEquals(true, handlerCalled[0]);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"campaignId": 5678,
"templateId": 8765,
"messageId": "background123456",
"isGhostPush": false,
"actionButtons": [
{
"identifier": "remindMeButton",
"title": "Remind me in 15 minutes",
"openApp": false,
"action": {
"type": "snoozeReminder",
"data": "{\"delay\":15}"
}
}
],
"defaultAction": {
"type": null
}
}
Loading