Skip to content

feat(W-23159744): per-user persistent feature flags (Android)#2937

Open
wmathurin wants to merge 19 commits into
forcedotcom:devfrom
wmathurin:W-21167151-feature-flags-per-user
Open

feat(W-23159744): per-user persistent feature flags (Android)#2937
wmathurin wants to merge 19 commits into
forcedotcom:devfrom
wmathurin:W-21167151-feature-flags-per-user

Conversation

@wmathurin

@wmathurin wmathurin commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements per-user persistent feature flags (BW / WD) on Android, with full UI test coverage including restart validation.

feat: Per-user persistent feature flags (W-21167151 / W-23159744)

  • SalesforceSDKManager.perUserFeatures: in-memory ConcurrentHashMap loaded from disk at process startup via hydratePerUserFeatures()
  • BW flag (browser-based / advanced auth) saved to disk per user at login; reloaded into user agent on next process start
  • WD flag (welcome discovery) saved to disk per user at login; reloaded into user agent on next process start
  • Register BW flag globally when Custom Tab launches (fixes missing flag when using advanced auth login host)
  • Fix NPE in hydratePerUserFeatures() during construction

feat: Display user agent in AuthFlowTester UI

  • User agent string shown in the app and validated in all UI tests via validateUserAgent()

feat: restartApp() helper and LoginWithRestartTests suite (new — 10 tests)

New restartApp() method in AuthFlowTest (base class) kills only the app process and relaunches it, so the SDK re-runs hydratePerUserFeatures() from disk — exercising the same code path as a real device restart. Uses pidof <package> filtered by myPid (the instrumentation runner's own PID) to avoid killing the test runner.

Built on top of restartApp(), new helper methods: restartAndValidateUser(), addOtherUserAndValidate(), switchToUserAndValidateUser().

New LoginWithRestartTests suite (full parity with iOS):

Test What it verifies
testCAOpaque_DefaultScopes_WithRestart CA Opaque session persists after restart
testECAOpaque_DefaultScopes_WithRestart ECA Opaque session persists after restart
testBeaconOpaque_DefaultScopes_WithRestart Beacon Opaque session + child key persists after restart
testECAJwt_DefaultScopes_DynamicConfiguration_WithRestart ECA JWT (dynamic config) session persists after restart
testECAJwt_SubsetScopes_DynamicConfiguration_WithRestart ECA JWT subset scopes session persists after restart
testBeaconJwt_DefaultScopes_DynamicConfiguration_WithRestart Beacon JWT (dynamic config) session persists after restart
testBeaconJwt_SubsetScopes_DynamicConfiguration_WithRestart Beacon JWT subset scopes session persists after restart
testAdvancedAuth_WithRestart BW feature flag persists in user agent after restart
testWelcomeDiscovery_WithRestart WD feature flag persists in user agent after restart
testMultiUserRestart Both users' sessions and flags persist after restart

Test plan

  • All 10 LoginWithRestartTests pass on a connected device/emulator
  • No regressions in existing LoginTests, MultiUserLoginTests, DynamicConfigTests, ScopeSelectionTests

Implements per-user feature flag storage and hydration so each
user account's active features are accurately reflected in the
User-Agent string across app restarts.

- UserAccount: featureFlags field (Set<String>), serialized to JSON,
  persisted via AccountManager (KEY_FEATURE_FLAGS, encrypted comma-separated)
- SalesforceSDKManager: perUserFeatures map (ConcurrentHashMap<String,ConcurrentSkipListSet>),
  getUserAgent(qualifier, user) unions global + per-user flags,
  registerUsedAppFeature/unregisterUsedAppFeature per-user overloads,
  hydratePerUserFeatures() called from init block,
  isGlobalFeatureRegistered() helper
- LoginActivity: completedViaBrowserTab flag captures Custom Tab completion
  (covers both regular and Login for Admin); onAuthFlowSuccess promotes
  BW/WD/QR transient global flags to per-user
- AuthenticationUtilities: BA/SL flags registered per-user
- SmartStoreSDKManager: SU flag registered per-user
- SyncManager/LayoutSyncManager/MetadataSyncManager: MS/Layout/Metadata flags per-user
- SalesforceHybridSDKManager: getUserAgent(qualifier, user) override
- UserAccountTest, SalesforceSDKManagerTests: new unit tests
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
3 Warnings
⚠️ libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java#L112 - Do not place Android context classes in static fields (static reference to UserAccountManager which has field context pointing to Context); this is a memory leak
⚠️ libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AuthenticatorService.java#L58 - Do not place Android context classes in static fields (static reference to Authenticator which has field context pointing to Context); this is a memory leak
⚠️ libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt#L709 - Switch statement on an int with known associated constant missing case BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE

Generated by 🚫 Danger

FEATURE_BROWSER_LOGIN must appear in the UA for requests made during
the login session. Register it globally in loadLoginPageInCustomTab()
(alongside completedViaBrowserTab = true), then clear global and
promote to per-user at onAuthFlowSuccess — matching the WD/QR pattern.
hydratePerUserFeatures() is called from the init block, before the
userAccountManager lazy delegate is set up. Call
UserAccountManager.getInstance() directly instead of going through
the lazy property.
…tion to UI tests

- Add USER_AGENT_CONTENT_DESC constant to AuthFlowTesterActivity
- Add getUserAgentString() helper and User Agent InfoRowView to UserCredentialsView
- Include User Agent in JSON export of credentials card
- Add contentDescription param to InfoRowView (defaults to label) to allow distinct semantic IDs
- Add validateUserAgent() to AuthFlowTesterPageObject: asserts SalesforceMobileSDK/ prefix,
  ftr_ segment, and BW/WD/MU flag presence/absence based on login config
- Thread isMultiUser param through loginAndValidate() and call validateUserAgent() after login
- Update RefreshTokenMigrationTests override to include new isMultiUser param
- Add loginOtherUserAndValidate/switchToUserAndValidate UA validation in MultiUserLoginTests
- Add testAdvancedAuthUser_HasBWFlag_RegularAuthUser_DoesNot test

Note: pre-existing WebView 5s timeout failures in BootConfigLoginTests,
NegativeLoginTests, RefreshTokenMigrationTests are infrastructure flakiness
unrelated to these changes.
…Agent into public/private overloads

validateUserAgent() fetches the UA once and delegates to a private overload
taking ua: String. validateUser() calls the private overload using credentials
already loaded, eliminating a second UI traversal.
…gisterUsedAppFeature

handleScreenLockPolicy and handleBiometricAuthPolicy now call
registerUsedAppFeature(feature, account) / unregisterUsedAppFeature(feature, account).
Update the 6 mock verifications accordingly.
@wmathurin

Copy link
Copy Markdown
Contributor Author

Retriggering CI — MobileSync and SalesforceHybrid failures are org rate-limit / connectivity flakes (SyncManagerTest.java:95 'Creates failed', 60s timeouts in MobileSyncJSTest), unrelated to this PR's changes. SalesforceSDK, SmartStore, and UI tests all pass.

…inForAdmin

LoginForAdminTests use a regular login host but browser-based auth, which
sets the BW flag. validateUserAgent was deciding BW solely from loginHostConfig,
causing LoginForAdminTests to fail. Now callers pass expectAdvancedAuth
(true when loginForAdmin || loginHostConfig == ADVANCED_AUTH) and the UA check
uses that instead of re-deriving from the host.
Makes the TODO actionable — re-enable once server enables Named JWTs for Hybrid Flows.
…estart

Adds LoginWithRestartTests with four tests mirroring iOS:
- testCAOpaque_WithRestart / testECAOpaque_WithRestart: session persistence baselines
- testAdvancedAuth_WithRestart: login via advanced auth (BW flag), force-stop, relaunch, assert BW persists
- testWelcomeDiscovery_WithRestart: login via welcome discovery (WD flag), force-stop, relaunch, assert WD persists

Also adds restartAndValidateUser helper to AuthFlowTest that uses `am force-stop`
via UiAutomator to fully kill the process before relaunching, exercising the same
persistence code path as a real device restart.
… restart helpers

AuthFlowTest:
- Add restartApp() helper (am force-stop + relaunch via explicit intent)
- restartAndValidateUser() now delegates to restartApp()
- Add addOtherUserAndValidate() and switchToUserAndValidateUser() helpers for multi-user restart tests

LoginWithRestartTests:
- Full parity with iOS: CA, ECA, Beacon (static + dynamic + subset scopes), advanced auth (BW), welcome discovery (WD), multi-user restart
am force-stop kills all processes in the package, including the test
runner which shares the app's package name. Switch to pidof + kill -9
filtered by myPid so only the app process is killed, leaving the
instrumentation alive.
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