Skip to content

[MOB-11508] Fix Android SDK auth token refresh in background #910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 25, 2025

Conversation

sumeruchat
Copy link
Collaborator

@sumeruchat sumeruchat commented Jun 5, 2025

Problem

Priceline was experiencing significantly higher request volumes (10x higher RPM) from the Android SDK compared to iOS, causing infrastructure strain. The root cause was that the Android SDK was aggressively refreshing JWT tokens even when the app was in the background, unlike the iOS SDK which naturally pauses token refreshes when the app is backgrounded.

Root Cause

The Android IterableAuthManager uses java.util.Timer which runs on its own background thread, independent of the app's lifecycle. This means the timer continues to fire and make token refresh requests even when the app is not active, whereas iOS timers are tied to the main run loop and pause when the app is suspended.

Solution

Added lifecycle awareness to IterableAuthManager by implementing IterableActivityMonitor.AppStateCallback:

  • Background transition: Clear the refresh timer to stop background token refresh requests
  • Foreground transition: Re-evaluate token state and resume appropriate refresh logic
  • Proper cleanup: Unregister from activity monitor when auth manager is reset

Changes Made

Core Implementation

  • IterableAuthManager.java:
    • Implement IterableActivityMonitor.AppStateCallback interface
    • Register for lifecycle callbacks in constructor
    • Add onSwitchToBackground() to clear timers
    • Add onSwitchToForeground() to resume token evaluation
    • Clean up callbacks in reset() method

Tests Added

  • IterableApiAuthTests.java: Added testAuthTokenRefreshPausesOnBackground() to verify lifecycle behavior
  • IterableActivityMonitorTest.java: Added testAuthManagerLifecycleRegistration() to verify proper registration/cleanup

Impact

  • Reduces RPM: Android SDK will no longer make aggressive background token refresh requests
  • iOS Parity: Brings Android behavior in line with iOS SDK behavior
  • Infrastructure Relief: Addresses Priceline's infrastructure strain from high request volumes
  • Maintains Functionality: Token refresh resumes appropriately when app returns to foreground

Testing

  • ✅ Unit tests pass for lifecycle behavior
  • ✅ Auth logic continues to work when app is active
  • ✅ Timer properly clears on background and resumes on foreground
  • ✅ No breaking changes to existing auth functionality

Ticket

Fixes https://iterable.atlassian.net/browse/MOB-11508

Additional Notes

This is a minimal, focused fix that addresses the specific issue without introducing unnecessary complexity. The changes align Android's behavior with iOS while maintaining all existing authentication functionality.

📱 Manual Testing Guidelines

Setup Requirements

  • Android device/emulator with API level 21+
  • Test app with Iterable SDK integration and JWT authentication enabled
  • Network monitoring tool (Charles Proxy, Flipper, or similar)
  • Ability to set custom JWT expiration times for testing

Test Case 1: Background Token Refresh Pause

Objective: Verify token refresh requests stop when app goes to background

Steps:

  1. Launch test app with valid JWT token
  2. Start network monitoring to capture requests to Iterable endpoints
  3. Ensure app is in foreground and token refresh timer is active
  4. Background the app (press home button or switch to another app)
  5. Wait 5-10 minutes while monitoring network traffic
  6. Expected Result: No JWT refresh requests should be made to /users/getByEmail or similar endpoints
  7. Failure Criteria: If you see periodic auth token refresh requests while app is backgrounded

Test Case 2: Foreground Token Refresh Resume

Objective: Verify token refresh resumes when app returns to foreground

Steps:

  1. With app backgrounded from Test Case 1
  2. Bring app back to foreground
  3. Monitor network traffic for next 30-60 seconds
  4. Expected Result:
    • If token is still valid: Timer should resume normal expiration-based refresh
    • If token expired while backgrounded: Should immediately request new token
  5. Failure Criteria: No token refresh activity after foregrounding with valid user

Test Case 3: Expired Token Handling

Objective: Verify expired tokens are refreshed immediately on foreground

Steps:

  1. Configure test environment with JWT that expires in 2-3 minutes
  2. Launch app and verify token is active
  3. Background app for 5+ minutes (longer than token expiration)
  4. Foreground the app
  5. Monitor network requests immediately after foregrounding
  6. Expected Result: Should see immediate token refresh request within 1-2 seconds
  7. Failure Criteria: No token refresh attempt, or user gets auth errors

Test Case 4: Rapid Background/Foreground Transitions

Objective: Verify system handles rapid transitions gracefully

Steps:

  1. Launch app with valid token
  2. Rapidly switch background/foreground 10+ times (home button, back to app)
  3. Monitor for any crashes or exceptions in logs
  4. Expected Result: No crashes, smooth transitions, timer properly managed
  5. Failure Criteria: App crashes, memory leaks, or excessive timer creation

Test Case 5: No User Set Scenarios

Objective: Verify behavior when no user email/ID is configured

Steps:

  1. Fresh app install (no user set)
  2. Background/foreground the app multiple times
  3. Set user email/ID while app is backgrounded
  4. Foreground the app
  5. Expected Result: Token refresh should begin after user is set and app is foregrounded
  6. Failure Criteria: Crashes or attempts to refresh without user context

Logging & Monitoring

Enable verbose logging to see auth manager activity:

adb shell setprop log.tag.IterableAuth VERBOSE

Key log messages to look for:

  • "App switched to background. Clearing auth refresh timer."
  • "App switched to foreground. Re-evaluating auth token refresh."
  • "Token expired, requesting new token on foreground"

Network monitoring checklist:

  • ✅ No background auth requests when app is inactive
  • ✅ Immediate token refresh on foreground if expired
  • ✅ Normal timer-based refresh for valid tokens
  • ✅ Proper error handling for network failures

@sumeruchat sumeruchat force-pushed the feature/MOB-11508-fix-auth-retry-logic branch from eebe6ea to e8ff15e Compare June 5, 2025 13:21
@Ayyanchira Ayyanchira requested a review from Copilot June 5, 2025 22:15
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes the aggressive background JWT token refresh in the Android SDK by adding lifecycle awareness to the IterableAuthManager so that refresh timers are cleared when the app is backgrounded and resumed when foregrounded.

  • Implements IterableActivityMonitor.AppStateCallback in IterableAuthManager
  • Adds tests to verify lifecycle behavior in IterableApiAuthTests and IterableActivityMonitorTest
  • Unregisters lifecycle callbacks on reset to prevent unintended refreshes

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java Adds test for pausing token refresh on background transition
iterableapi/src/test/java/com/iterable/iterableapi/IterableActivityMonitorTest.java Verifies proper lifecycle registration and unregistration
iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java Implements lifecycle callbacks to clear and resume token refresh timers
Comments suppressed due to low confidence (1)

iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java:17

  • [nitpick] For consistency with Java constant naming conventions, consider renaming 'expirationString' to 'EXPIRATION_STRING'.
private static final String expirationString = "exp";

Comment on lines +271 to +287
String authToken = api.getAuthToken();

if (authToken != null) {
queueExpirationRefresh(authToken);
// If queueExpirationRefresh didn't schedule a timer (expired token case), request new token
if (!isTimerScheduled && !pendingAuth) {
IterableLogger.d(TAG, "Token expired, requesting new token on foreground");
requestNewAuthToken(false, null, true);
}
} else if ((api.getEmail() != null || api.getUserId() != null) && !pendingAuth) {
IterableLogger.d(TAG, "App foregrounded, user identified, no auth token present. Requesting new token.");
requestNewAuthToken(false, null, true);
}
} catch (Exception e) {
IterableLogger.e(TAG, "Error in onSwitchToForeground", e);
}
}

This comment was marked as resolved.

Copy link
Member

@Ayyanchira Ayyanchira left a comment

Choose a reason for hiding this comment

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

Adding lifecycle methods has added a bit bloat in this manager class, but seems necessary to have it handled by itself different flows that can occur.
Here's a loom link of what I tested


// After reset, lifecycle methods should still work (no exceptions)
authManager.onSwitchToBackground();
authManager.onSwitchToForeground();
Copy link
Member

Choose a reason for hiding this comment

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

Probably a better check would be to trigger parent level onSwitchToBackground to check if authmanager's onSwitchToBackground is actually called or not to check the unregistering of callbacks.

authManager.onSwitchToForeground();
shadowOf(getMainLooper()).runToEndOfTasks();

// Test passes if no exceptions were thrown and lifecycle methods executed successfully
Copy link
Member

Choose a reason for hiding this comment

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

Should we not test anything after onSwitchToForeground? Like if the jwt is being fetched again or not? Or if the timer is no more running. Or probably running as there is new requests SDK is making?

public void onSwitchToForeground() {
try {
IterableLogger.v(TAG, "App switched to foreground. Re-evaluating auth token refresh.");
String authToken = api.getAuthToken();

This comment was marked as outdated.

@sumeruchat sumeruchat merged commit 6645080 into master Jun 25, 2025
2 of 3 checks passed
@sumeruchat sumeruchat deleted the feature/MOB-11508-fix-auth-retry-logic branch June 25, 2025 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants