Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,15 @@ import com.salesforce.androidsdk.auth.idp.interfaces.IDPManager
import com.salesforce.androidsdk.auth.idp.interfaces.SPManager
import com.salesforce.androidsdk.config.AdminPermsManager
import com.salesforce.androidsdk.config.AdminSettingsManager
import com.salesforce.androidsdk.config.BootConfig
import com.salesforce.androidsdk.config.BootConfig.getBootConfig
import com.salesforce.androidsdk.config.LoginServerManager
import com.salesforce.androidsdk.config.LoginServerManager.PRODUCTION_LOGIN_URL
import com.salesforce.androidsdk.config.LoginServerManager.SANDBOX_LOGIN_URL
import com.salesforce.androidsdk.config.LoginServerManager.WELCOME_LOGIN_URL
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.IDPAppPackageName
import com.salesforce.androidsdk.config.RuntimeConfig.getRuntimeConfig
import com.salesforce.androidsdk.developer.support.DevSupportInfo
import com.salesforce.androidsdk.developer.support.notifications.local.ShowDeveloperSupportNotifier.Companion.BROADCAST_INTENT_ACTION_SHOW_DEVELOPER_SUPPORT
import com.salesforce.androidsdk.developer.support.notifications.local.ShowDeveloperSupportNotifier.Companion.hideDeveloperSupportNotification
import com.salesforce.androidsdk.developer.support.notifications.local.ShowDeveloperSupportNotifier.Companion.showDeveloperSupportNotification
Expand Down Expand Up @@ -1338,6 +1340,10 @@ open class SalesforceSDKManager protected constructor(
}

/** Information to display in the developer support dialog */
@Deprecated(
"Will be removed in Mobile SDK 14.0, please use the new data class representation.",
ReplaceWith("devSupportInfo")
)
open val devSupportInfos: List<String>
get() = mutableListOf(
"SDK Version", SDK_VERSION,
Expand Down Expand Up @@ -1377,6 +1383,25 @@ open class SalesforceSDKManager protected constructor(
}
}

open val devSupportInfo
get() = DevSupportInfo(
SDK_VERSION,
appType,
userAgent,
userAccountManager.authenticatedUsers ?: emptyList(),
authConfig = listOf(
"Use Web Server Authentication" to "$useWebServerAuthentication",
"Use Hybrid Authentication Token" to "$useHybridAuthentication",
"Support Welcome Discovery" to "$supportsWelcomeDiscovery",
"Browser Login Enabled" to "$isBrowserLoginEnabled",
"IDP Enabled" to "$isIDPLoginFlowEnabled",
"Identity Provider" to "$isIdentityProvider",
),
BootConfig.getBootConfig(appContext),
userAccountManager.currentUser,
getRuntimeConfig(appContext),
)

private fun accessTokenExpiration(): String {
val currentUser = userAccountManager.cachedCurrentUser
var expiration = "Unknown"
Expand Down Expand Up @@ -1418,6 +1443,20 @@ open class SalesforceSDKManager protected constructor(
return devInfos
}

/**
* Returns a string representation of the provided users.
* @param userAccounts The user accounts
* @return A string representation of the provided users.
*/
private fun usersToString(
vararg userAccounts: UserAccount
) = join(
", ",
userAccounts.map { userAccount ->
userAccount.accountName
}
)

/**
* Returns a string representation of the provided users.
* @param userAccounts The user accounts
Expand All @@ -1426,9 +1465,7 @@ open class SalesforceSDKManager protected constructor(
private fun usersToString(
userAccounts: List<UserAccount>?
) = userAccounts?.toTypedArray<UserAccount>()?.let {
join(", ", it.map { userAccount ->
userAccount.accountName
})
usersToString(*it)
} ?: ""

/** Sends the logout completed intent */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.salesforce.androidsdk.developer.support

import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.auth.JwtAccessToken
import com.salesforce.androidsdk.config.BootConfig
import com.salesforce.androidsdk.config.RuntimeConfig
import java.text.SimpleDateFormat
import java.util.Locale

data class DevSupportInfo(
Copy link
Contributor

@wmathurin wmathurin Nov 14, 2025

Choose a reason for hiding this comment

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

On iOS I kept the Dev Info "dumb" to be able to support arbitrary grouping of rows - including rows provided by the app or subclasses of SalesforceSDKManager e.g. SmartStoreSDKManager here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm, I wanted to keep as much of the sorting logic out of the UI as I could... but I forgot about the subclasses. It would be easy to add the subclasses data as additional categories, but if you think I should revert back to the old simple list I can.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right now the dev infos provided by subclasses of SalesforceSDKManager are no longer shown (e.g. https://github.com/forcedotcom/SalesforceMobileSDK-Android/blob/dev/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/app/SmartStoreSDKManager.java#L470) .
At least we should fix that but we would still be breaking apps that implemented their own SDKManager and overrode getDevSupportInfos.

That's why I ended up going with the list that has "section:xxx" markers. It's a bit low tech ;-) but it keeps everything working.

Maybe the move to a typed dev infos could be done in 14.0??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DevSupportInfo

var smartStoreValues: List<Pair<String, String>>? = null

SmartStoreSDKManger

@Override
    public @NotNull DevSupportInfo getDevSupportInfo() {
        DevSupportInfo devInfo = super.getDevSupportInfo();
        devInfo.setSmartStoreValues(Arrays.asList(
                new Pair<>("SQLCipher version", getSmartStore().getSQLCipherVersion()),
                new Pair<>("SQLCipher Compile Options", TextUtils.join(", ", getSmartStore().getCompileOptions())),
                new Pair<>("SQLCipher Runtime Setting", TextUtils.join(", ", getSmartStore().getRuntimeSettings())),
                new Pair<>("User SmartStores", TextUtils.join(", ", getUserStoresPrefixList())),
                new Pair<>("Global SmartStores", TextUtils.join(", ", getGlobalStoresPrefixList())),
                new Pair<>("User Key-Value Stores", TextUtils.join(", ", getKeyValueStoresPrefixList())),
                new Pair<>("Global Key-Value Stores", TextUtils.join(", ", getGlobalKeyValueStoresPrefixList()))
        ));
        return devInfo;
    }

I was thinking this would be okay? The idea of grouping values based on a string that could change feels fragile but I will take a closer look at the iOS implementation before committing anything.

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. DevSupportInfo should not know about SmartStore.
  2. That would work for SmartStoreSDKManager but would still break apps (if they exist) that override getDevSupportInfos().

Maybe DevSupportInfo could have a field for other infos which would be shown in the UI after everything else.
The DevSupportInfo that you create in SalesforceSDKManager could populate that new field by calling the existing getDevSupportInfos().
The getDevSupportInfos() method in SalesforceSDKManager would return an empty list.
SmartStoreSDKManager and other managers could be left unchanged and their additional infos would keep showing up in the UI.

If we want some grouping in the additional infos, we could do the "section:xxx" hack I did on iOS or do something more structured (but that would mean changing the subclasses to take advantage of it).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. DevSupportInfo should not know about SmartStore.

I think if SmartStore data is optional it isn't a problem. The UI has to check if SmartStore data exists one way or another, this is just more structured.

  1. That would work for SmartStoreSDKManager but would still break apps (if they exist) that override getDevSupportInfos().

Good point. I am going to try to massage the list from the existing API (even if it has been modified by a subclass) into the DevSupportInfo class. If I make everything nullable and allow additional sections to be added it should work well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wmathurin I have the approach in my latest commit. The DevSupportInfo class now allows for everything to be null on the main constructor, has a secondary constructor similar to the original constructor and features a createFromLegacyDevInfos function to instantiate the class from List<String>. A unit test insures that given the same input data the objects created by the secondary constructor and the createFromLegacyDevInfos function are identical.

So whatever is in the devSupportInfos (list of string) will be parsed to the expected categories. Removed values simply will not show up, completely empty sections will now show up and any additional info will be added to the first basic non-collapsable section. SmartStoreSDKManager's override of devSupportInfos works as expected, creating a new "Smart Store" section.

Code to be used post 14.0 is commented out in place.

val sdkVersion: String,
val appType: String,
val userAgent: String,
val authenticatedUsers: List<UserAccount>,
val authConfig: List<Pair<String, String>>,
val bootConfig: BootConfig,
val currentUser: UserAccount?,
val runtimeConfig: RuntimeConfig,
) {
val bootConfigValues: List<Pair<String, String>> by lazy {
with(bootConfig) {
val values = mutableListOf(
"Consumer Key" to remoteAccessConsumerKey,
"Redirect URI" to oauthRedirectURI,
"Scopes" to oauthScopes.joinToString(separator = " "),
)

if (appType == "Hybrid") {
values.addAll(
listOf(
"Local" to isLocal.toString(),
"Start Page" to startPage,
"Unauthenticated Start Page" to unauthenticatedStartPage,
"Error Page" to errorPage,
"Should Authenticate" to shouldAuthenticate().toString(),
"Attempt Offline Load" to attemptOfflineLoad().toString(),
)
)
}

return@lazy values
}
}

val authenticatedUsersString: String = authenticatedUsers.joinToString(separator = ",\n") {
"${it.displayName} (${it.username})"
}

val currentUserInfo: List<Pair<String, String>> by lazy {
if (currentUser != null) {
with(currentUser) {
return@lazy mutableListOf(
"Username" to username,
"Consumer Key" to clientId,
"Scopes" to scope,
"Instance URL" to instanceServer,
"Token Format" to tokenFormat,
"Access Token Expiration" to accessTokenExpiration,
"Beacon Child Consumer Key" to beaconChildConsumerKey,
)
}
} else {
emptyList()
}
}

val runtimeConfigValues: List<Pair<String, String>> by lazy {
with(runtimeConfig) {
val values = mutableListOf(
"Managed App" to isManagedApp.toString()
)

if (isManagedApp) {
values.addAll(listOf(
"OAuth ID" to (getString(RuntimeConfig.ConfigKey.ManagedAppOAuthID) ?: "N/A"),
"Callback URL" to (getString(RuntimeConfig.ConfigKey.ManagedAppCallbackURL) ?: "N/A"),
"Require Cert Auth" to getBoolean(RuntimeConfig.ConfigKey.RequireCertAuth).toString(),
"Only Show Authorized Hosts" to getBoolean(RuntimeConfig.ConfigKey.OnlyShowAuthorizedHosts).toString(),
))
}

return@lazy values
}
}

val accessTokenExpiration: String
get() {
var expiration = "Unknown"

if (currentUser?.tokenFormat == "jwt") {
val jwtAccessToken = JwtAccessToken(currentUser.authToken)
val expirationDate = jwtAccessToken.expirationDate()
if (expirationDate != null) {
val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
expiration = dateFormatter.format(expirationDate)
}
}

return expiration
}
}
Loading