Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ class LayoutSyncManager private constructor(
store,
syncManager
).also { INSTANCES[uniqueId] = it }
SalesforceSDKManager.getInstance().registerUsedAppFeature(Features.FEATURE_LAYOUT_SYNC)
SalesforceSDKManager.getInstance().registerUsedAppFeature(Features.FEATURE_LAYOUT_SYNC, user)
return instance
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ class MetadataSyncManager private constructor(
INSTANCES[uniqueId] = it
}
SalesforceSDKManager.getInstance()
.registerUsedAppFeature(Features.FEATURE_METADATA_SYNC)
.registerUsedAppFeature(Features.FEATURE_METADATA_SYNC, user)
return instance
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ class SyncManager private constructor(smartStore: SmartStore, restClient: RestCl
instance = SyncManager(store, restClient)
instance.also { INSTANCES[uniqueId] = it }
}
SalesforceSDKManager.getInstance().registerUsedAppFeature(Features.FEATURE_MOBILE_SYNC)
SalesforceSDKManager.getInstance().registerUsedAppFeature(Features.FEATURE_MOBILE_SYNC, user)
return instance
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import androidx.annotation.NonNull;

import com.salesforce.androidsdk.accounts.UserAccount;
import com.salesforce.androidsdk.config.BootConfig;
import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager;
import com.salesforce.androidsdk.mobilesync.config.SyncsConfig;
Expand Down Expand Up @@ -148,13 +149,19 @@ public static SalesforceHybridSDKManager getInstance() {
@NonNull
@Override
public final String getUserAgent(@NonNull String qualifier) {
return getUserAgent(qualifier, null);
}

@NonNull
@Override
public final String getUserAgent(@NonNull String qualifier, @androidx.annotation.Nullable UserAccount user) {
final BootConfig config = BootConfig.getBootConfig(context);
if (config.isLocal()) {
qualifier = qualifier + "Local";
} else {
qualifier = qualifier + "Remote";
}
return super.getUserAgent(qualifier);
return super.getUserAgent(qualifier, user);
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@
import org.json.JSONObject;

import java.io.File;
import java.util.Collections;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* This class represents a single user account that is currently
Expand Down Expand Up @@ -100,6 +104,7 @@ public class UserAccount {
public static final String BEACON_CHILD_CONSUMER_KEY = "auto_installed_app_org_consumer_key";
public static final String BEACON_CHILD_CONSUMER_SECRET = "auto_installed_app_org_consumer_secret";
public static final String SCOPE = "scope";
public static final String FEATURE_FLAGS = "feature_flags";

private static final String TAG = "UserAccount";
private static final String FORWARD_SLASH = "/";
Expand Down Expand Up @@ -147,6 +152,7 @@ public class UserAccount {
private String beaconChildConsumerKey;
private String beaconChildConsumerSecret;
private String scope;
private Set<String> featureFlags = new java.util.HashSet<>();

/**
* Parameterized constructor.
Expand Down Expand Up @@ -760,6 +766,24 @@ public Map<String, String> getAdditionalOauthValues() {
return additionalOauthValues;
}

/**
* Returns the persisted per-user feature flags (e.g. BW, SU, MS).
*
* @return Unmodifiable set of feature flag codes.
*/
public Set<String> getFeatureFlags() {
return Collections.unmodifiableSet(featureFlags);
}

/**
* Replaces the in-memory set of persisted per-user feature flags.
*
* @param flags The new set of feature flags.
*/
public void setFeatureFlags(Set<String> flags) {
featureFlags = flags != null ? new HashSet<>(flags) : new HashSet<>();
}

/**
* Fetches this user's profile photo from the cache.
*
Expand Down Expand Up @@ -1024,6 +1048,11 @@ JSONObject toJson(List<String> additionalOauthKeys) {
object.put(BEACON_CHILD_CONSUMER_KEY, beaconChildConsumerKey);
object.put(BEACON_CHILD_CONSUMER_SECRET, beaconChildConsumerSecret);
object.put(SCOPE, scope);
if (!featureFlags.isEmpty()) {
org.json.JSONArray flagsArray = new org.json.JSONArray();
for (String f : featureFlags) flagsArray.put(f);
object.put(FEATURE_FLAGS, flagsArray);
}
object = MapUtil.addMapToJSONObject(additionalOauthValues, additionalOauthKeys, object);
} catch (JSONException e) {
SalesforceSDKLogger.e(TAG, "Unable to convert to JSON", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@
import com.salesforce.androidsdk.util.SalesforceSDKLogger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* This class acts as a manager that provides methods to access
Expand Down Expand Up @@ -553,6 +556,7 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
final String beaconChildConsumerKey = decryptUserData(account, AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_KEY, encryptionKey);
final String beaconChildConsumerSecret = decryptUserData(account, AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_SECRET, encryptionKey);
final String scope = decryptUserData(account, AuthenticatorService.KEY_SCOPE, encryptionKey);
final String featureFlagsRaw = decryptUserData(account, AuthenticatorService.KEY_FEATURE_FLAGS, encryptionKey);

Map<String, String> additionalOauthValues = null;
List<String> additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys();
Expand All @@ -571,7 +575,7 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
if (authToken == null || instanceServer == null || userId == null || orgId == null) {
return null;
} else {
return UserAccountBuilder.getInstance()
final UserAccount userAccount = UserAccountBuilder.getInstance()
.authToken(authToken)
.refreshToken(refreshToken)
.loginServer(loginServer)
Expand Down Expand Up @@ -610,6 +614,10 @@ public Bundle updateAccount(Account account, UserAccount userAccount) {
.scope(scope)
.additionalOauthValues(additionalOauthValues)
.build();
if (!TextUtils.isEmpty(featureFlagsRaw)) {
userAccount.setFeatureFlags(new HashSet<>(Arrays.asList(featureFlagsRaw.split(","))));
}
return userAccount;
}
}

Expand Down Expand Up @@ -758,6 +766,11 @@ private Bundle buildAuthBundle(UserAccount userAccount) {
extras.putString(AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_KEY, SalesforceSDKManager.encrypt(userAccount.getBeaconChildConsumerKey(), encryptionKey));
extras.putString(AuthenticatorService.KEY_BEACON_CHILD_CONSUMER_SECRET, SalesforceSDKManager.encrypt(userAccount.getBeaconChildConsumerSecret(), encryptionKey));
extras.putString(AuthenticatorService.KEY_SCOPE, SalesforceSDKManager.encrypt(userAccount.getScope(), encryptionKey));
final Set<String> featureFlags = userAccount.getFeatureFlags();
if (!featureFlags.isEmpty()) {
extras.putString(AuthenticatorService.KEY_FEATURE_FLAGS,
SalesforceSDKManager.encrypt(android.text.TextUtils.join(",", featureFlags), encryptionKey));
}

final List<String> additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys();
if (additionalOauthKeys != null && !additionalOauthKeys.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ import java.net.URI
import java.util.Locale.US
import java.util.SortedSet
import java.util.UUID.randomUUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentSkipListSet
import java.util.regex.Pattern
import com.salesforce.androidsdk.auth.idp.IDPManager as DefaultIDPManager
Expand Down Expand Up @@ -408,6 +409,9 @@ open class SalesforceSDKManager protected constructor(
/** App feature codes for reporting in the user agent header */
private val features: SortedSet<String?>

/** Per-user feature codes keyed by "orgId/userId" */
private val perUserFeatures: ConcurrentHashMap<String, ConcurrentSkipListSet<String>> = ConcurrentHashMap()

/**
* An additional list of OAuth keys to fetch and store from the token
* endpoint
Expand Down Expand Up @@ -1272,8 +1276,24 @@ open class SalesforceSDKManager protected constructor(
* @param qualifier The user agent qualifier
* @return The user agent string to use for all requests
*/
open fun getUserAgent(qualifier: String) =
String.format(
open fun getUserAgent(qualifier: String) = getUserAgent(qualifier, null)

/**
* Returns a per-user agent string. Feature flags include both global and user-specific codes.
*
* @param qualifier The user agent qualifier
* @param user The user account, or null to use the current user
* @return The user agent string to use for all requests
*/
open fun getUserAgent(qualifier: String, user: UserAccount?) : String {
val resolvedUser = user ?: userAccountManager.currentUser
val userKey = resolvedUser?.let { "${it.orgId}/${it.userId}" }
val userFeatures = userKey?.let { perUserFeatures[it] } ?: emptySet<String>()
val allFeatures = ConcurrentSkipListSet<String>(CASE_INSENSITIVE_ORDER).apply {
addAll(features.filterNotNull())
addAll(userFeatures)
}
return String.format(
"SalesforceMobileSDK/%s android mobile/%s (%s) %s/%s %s uid_%s ftr_%s SecurityPatch/%s",
SDK_VERSION,
RELEASE,
Expand All @@ -1282,9 +1302,10 @@ open class SalesforceSDKManager protected constructor(
appVersion,
"$appType$qualifier",
deviceId,
join(".", features),
join(".", allFeatures),
SECURITY_PATCH
)
}

/** The app version */
val appVersion: String
Expand Down Expand Up @@ -1321,6 +1342,59 @@ open class SalesforceSDKManager protected constructor(
fun unregisterUsedAppFeature(appFeatureCode: String?) =
features.remove(appFeatureCode)

/**
* Returns true if the feature code is in the global (non-user-specific) set.
* @param appFeatureCode The app feature code
*/
fun isGlobalFeatureRegistered(appFeatureCode: String) = features.contains(appFeatureCode)

/**
* Adds a per-user app feature code for reporting in the user agent header.
* Falls back to the global set when user is null.
* @param appFeatureCode The app feature code
* @param user The user account to associate the feature with
*/
fun registerUsedAppFeature(appFeatureCode: String, user: UserAccount?) {
if (user == null) { registerUsedAppFeature(appFeatureCode); return }
val key = "${user.orgId}/${user.userId}"
val set = perUserFeatures.getOrPut(key) { ConcurrentSkipListSet(CASE_INSENSITIVE_ORDER) }
set.add(appFeatureCode)
persistUserFeatureFlags(user, set)
}

/**
* Removes a per-user app feature code from reporting in the user agent header.
* Falls back to the global set when user is null.
* @param appFeatureCode The app feature code
* @param user The user account to remove the feature from
*/
fun unregisterUsedAppFeature(appFeatureCode: String, user: UserAccount?) {
if (user == null) { unregisterUsedAppFeature(appFeatureCode); return }
val key = "${user.orgId}/${user.userId}"
perUserFeatures[key]?.remove(appFeatureCode)
persistUserFeatureFlags(user, perUserFeatures[key] ?: emptySet())
}

private fun persistUserFeatureFlags(user: UserAccount, flags: Set<String>) {
user.featureFlags = HashSet(flags)
val account = userAccountManager.buildAccount(user) ?: return
userAccountManager.updateAccount(account, user)
}

/** Hydrates per-user features from persisted accounts at startup */
private fun hydratePerUserFeatures() {
val users = userAccountManager.authenticatedUsers ?: return
for (u in users) {
val flags = u.featureFlags
if (flags.isNotEmpty()) {
val key = "${u.orgId}/${u.userId}"
val set = ConcurrentSkipListSet<String>(CASE_INSENSITIVE_ORDER)
set.addAll(flags)
perUserFeatures[key] = set
}
}
}

/** The app type */
open val appType = "Native"

Expand Down Expand Up @@ -1788,6 +1862,9 @@ open class SalesforceSDKManager protected constructor(
nativeLoginActivity,
googleCloudProjectId,
)
// Hydrate after INSTANCE is set — UserAccountManager.getInstance() checks
// SalesforceSDKManager.getInstance() internally, which requires INSTANCE != null.
INSTANCE?.hydratePerUserFeatures()
}
initInternal(context)
EventsObservable.get().notifyEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,15 +391,15 @@ internal fun handleScreenLockPolicy(

// compareTo(0) is used to check if screenLockTimeout is non-null and greater than 0.
if (userIdentity?.screenLockTimeout?.compareTo(0) == 1) {
SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_SCREEN_LOCK)
SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_SCREEN_LOCK, account)
val timeoutInMills = userIdentity.screenLockTimeout * 1000 * 60
internalScreenLockManager?.storeMobilePolicy(
account,
enabled = userIdentity.screenLock,
timeoutInMills,
)
} else if (internalScreenLockManager?.enabled == true) {
SalesforceSDKManager.getInstance().unregisterUsedAppFeature(FEATURE_SCREEN_LOCK)
SalesforceSDKManager.getInstance().unregisterUsedAppFeature(FEATURE_SCREEN_LOCK, account)
internalScreenLockManager.cleanUp(account)
}
}
Expand All @@ -416,15 +416,15 @@ internal fun handleBiometricAuthPolicy(
SalesforceSDKManager.getInstance().biometricAuthenticationManager as BiometricAuthenticationManager?

if (userIdentity?.biometricAuth == true) {
SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_BIOMETRIC_AUTH)
SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_BIOMETRIC_AUTH, account)
val timeoutInMills = userIdentity.biometricAuthTimeout * 60 * 1000
internalBiometricAuthenticationManager?.storeMobilePolicy(
account,
enabled = userIdentity.biometricAuth,
timeoutInMills
)
} else if (internalBiometricAuthenticationManager?.enabled == true) {
SalesforceSDKManager.getInstance().unregisterUsedAppFeature(FEATURE_BIOMETRIC_AUTH)
SalesforceSDKManager.getInstance().unregisterUsedAppFeature(FEATURE_BIOMETRIC_AUTH, account)
internalBiometricAuthenticationManager.cleanUp(account)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public class AuthenticatorService extends Service {
public static final String KEY_BEACON_CHILD_CONSUMER_KEY = "auto_installed_app_org_consumer_key";
public static final String KEY_BEACON_CHILD_CONSUMER_SECRET = "auto_installed_app_org_consumer_secret";
public static final String KEY_SCOPE = "scope";
public static final String KEY_FEATURE_FLAGS = "feature_flags";

private static final String TAG = "AuthenticatorService";

Expand Down
Loading
Loading