From ce988daaaf57c90fa3d15b6e354cf0fba689e02d Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 22 Jun 2023 15:08:27 -0700 Subject: [PATCH 01/24] Updating version numbers to 11.1.0 --- README.md | 2 +- build.gradle | 2 +- external/shared | 2 +- libs/MobileSync/AndroidManifest.xml | 4 ++-- libs/MobileSync/build.gradle | 2 +- libs/SalesforceAnalytics/AndroidManifest.xml | 4 ++-- libs/SalesforceAnalytics/build.gradle | 2 +- libs/SalesforceHybrid/AndroidManifest.xml | 4 ++-- libs/SalesforceHybrid/build.gradle | 2 +- libs/SalesforceHybrid/res/xml/config.xml | 2 +- libs/SalesforceReact/AndroidManifest.xml | 4 ++-- libs/SalesforceReact/build.gradle | 2 +- libs/SalesforceReact/package.json | 4 ++-- libs/SalesforceSDK/AndroidManifest.xml | 4 ++-- libs/SalesforceSDK/build.gradle | 2 +- .../com/salesforce/androidsdk/app/SalesforceSDKManager.java | 2 +- libs/SmartStore/AndroidManifest.xml | 4 ++-- libs/SmartStore/build.gradle | 2 +- libs/test/SalesforceHybridTest/res/xml/config.xml | 2 +- package.json | 2 +- tools/generate_doc.sh | 2 +- 21 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 93c2305f2b..381c313c8d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This pulls submodule dependencies from github. Introduction == -### What's New in 11.0.0 +### What's New in 11.1.0 See [release notes](https://github.com/forcedotcom/SalesforceMobileSDK-Android/releases). ### Native Applications diff --git a/build.gradle b/build.gradle index 9b318f0303..0943d9b3aa 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { allprojects { group = 'com.salesforce.mobilesdk' - version = '11.0.0' + version = '11.1.0' repositories { mavenLocal() maven { diff --git a/external/shared b/external/shared index 2599472da9..07e73145e2 160000 --- a/external/shared +++ b/external/shared @@ -1 +1 @@ -Subproject commit 2599472da9b7503109717aff08c07afa2f2cf4af +Subproject commit 07e73145e29510675be7f68a0191ac9d39dc7195 diff --git a/libs/MobileSync/AndroidManifest.xml b/libs/MobileSync/AndroidManifest.xml index ab4992f668..b38eb0eca8 100644 --- a/libs/MobileSync/AndroidManifest.xml +++ b/libs/MobileSync/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="81" + android:versionName="11.1.0.dev"> diff --git a/libs/MobileSync/build.gradle b/libs/MobileSync/build.gradle index 6ef273426b..16c5ff430d 100644 --- a/libs/MobileSync/build.gradle +++ b/libs/MobileSync/build.gradle @@ -76,7 +76,7 @@ android { ext { PUBLISH_GROUP_ID = 'com.salesforce.mobilesdk' - PUBLISH_VERSION = '11.0.0' + PUBLISH_VERSION = '11.1.0' PUBLISH_ARTIFACT_ID = 'MobileSync' } diff --git a/libs/SalesforceAnalytics/AndroidManifest.xml b/libs/SalesforceAnalytics/AndroidManifest.xml index 88b928bba4..e752fdf1ef 100644 --- a/libs/SalesforceAnalytics/AndroidManifest.xml +++ b/libs/SalesforceAnalytics/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="81" + android:versionName="11.1.0.dev"> diff --git a/libs/SalesforceAnalytics/build.gradle b/libs/SalesforceAnalytics/build.gradle index 114c6810ac..0bcb6a68f9 100644 --- a/libs/SalesforceAnalytics/build.gradle +++ b/libs/SalesforceAnalytics/build.gradle @@ -69,7 +69,7 @@ android { ext { PUBLISH_GROUP_ID = 'com.salesforce.mobilesdk' - PUBLISH_VERSION = '11.0.0' + PUBLISH_VERSION = '11.1.0' PUBLISH_ARTIFACT_ID = 'SalesforceAnalytics' } diff --git a/libs/SalesforceHybrid/AndroidManifest.xml b/libs/SalesforceHybrid/AndroidManifest.xml index c5f0d3ab3b..d4150c2e70 100644 --- a/libs/SalesforceHybrid/AndroidManifest.xml +++ b/libs/SalesforceHybrid/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="81" + android:versionName="11.1.0.dev"> diff --git a/libs/SalesforceHybrid/build.gradle b/libs/SalesforceHybrid/build.gradle index 1e2d4aaed6..dd7e1e3336 100644 --- a/libs/SalesforceHybrid/build.gradle +++ b/libs/SalesforceHybrid/build.gradle @@ -73,7 +73,7 @@ android { ext { PUBLISH_GROUP_ID = 'com.salesforce.mobilesdk' - PUBLISH_VERSION = '11.0.0' + PUBLISH_VERSION = '11.1.0' PUBLISH_ARTIFACT_ID = 'SalesforceHybrid' } diff --git a/libs/SalesforceHybrid/res/xml/config.xml b/libs/SalesforceHybrid/res/xml/config.xml index d13cd3f568..f749986187 100644 --- a/libs/SalesforceHybrid/res/xml/config.xml +++ b/libs/SalesforceHybrid/res/xml/config.xml @@ -1,7 +1,7 @@ + version = "11.1.0"> diff --git a/libs/SalesforceReact/AndroidManifest.xml b/libs/SalesforceReact/AndroidManifest.xml index c5f0d3ab3b..d4150c2e70 100644 --- a/libs/SalesforceReact/AndroidManifest.xml +++ b/libs/SalesforceReact/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="81" + android:versionName="11.1.0.dev"> diff --git a/libs/SalesforceReact/build.gradle b/libs/SalesforceReact/build.gradle index bc350744a1..05e7d692eb 100644 --- a/libs/SalesforceReact/build.gradle +++ b/libs/SalesforceReact/build.gradle @@ -124,7 +124,7 @@ afterEvaluate { ext { PUBLISH_GROUP_ID = 'com.salesforce.mobilesdk' - PUBLISH_VERSION = '11.0.0' + PUBLISH_VERSION = '11.1.0' PUBLISH_ARTIFACT_ID = 'SalesforceReact' } diff --git a/libs/SalesforceReact/package.json b/libs/SalesforceReact/package.json index becd2116d1..8f9f0a6a06 100644 --- a/libs/SalesforceReact/package.json +++ b/libs/SalesforceReact/package.json @@ -1,6 +1,6 @@ { "name": "SalesforceReact", - "version": "11.0.0", + "version": "11.1.0", "private": true, "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start" @@ -9,7 +9,7 @@ "create-react-class": "^15.7.0", "react": "18.1.0", "react-native": "0.70.6", - "react-native-force": "git+https://github.com/forcedotcom/SalesforceMobileSDK-ReactNative.git#v11.0.0" + "react-native-force": "git+https://github.com/forcedotcom/SalesforceMobileSDK-ReactNative.git#dev" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/libs/SalesforceSDK/AndroidManifest.xml b/libs/SalesforceSDK/AndroidManifest.xml index 37d540c5ce..6b6eea8c12 100644 --- a/libs/SalesforceSDK/AndroidManifest.xml +++ b/libs/SalesforceSDK/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="81" + android:versionName="11.1.0.dev"> diff --git a/libs/SalesforceSDK/build.gradle b/libs/SalesforceSDK/build.gradle index 4d12dfaec1..851feecbb7 100644 --- a/libs/SalesforceSDK/build.gradle +++ b/libs/SalesforceSDK/build.gradle @@ -77,7 +77,7 @@ android { ext { PUBLISH_GROUP_ID = 'com.salesforce.mobilesdk' - PUBLISH_VERSION = '11.0.0' + PUBLISH_VERSION = '11.1.0' PUBLISH_ARTIFACT_ID = 'SalesforceSDK' } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java index 9f5d25657e..2fc7ef9d56 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java @@ -114,7 +114,7 @@ public class SalesforceSDKManager implements LifecycleObserver { /** * Current version of this SDK. */ - public static final String SDK_VERSION = "11.0.0"; + public static final String SDK_VERSION = "11.1.0.dev"; /** * Intent action meant for instances of SalesforceSDKManager residing in other processes diff --git a/libs/SmartStore/AndroidManifest.xml b/libs/SmartStore/AndroidManifest.xml index 38af2c8a59..0325a5a267 100644 --- a/libs/SmartStore/AndroidManifest.xml +++ b/libs/SmartStore/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="81" + android:versionName="11.1.0.dev"> diff --git a/libs/SmartStore/build.gradle b/libs/SmartStore/build.gradle index 0d3746f598..34cfa1a8a1 100644 --- a/libs/SmartStore/build.gradle +++ b/libs/SmartStore/build.gradle @@ -75,7 +75,7 @@ android { ext { PUBLISH_GROUP_ID = 'com.salesforce.mobilesdk' - PUBLISH_VERSION = '11.0.0' + PUBLISH_VERSION = '11.1.0' PUBLISH_ARTIFACT_ID = 'SmartStore' } diff --git a/libs/test/SalesforceHybridTest/res/xml/config.xml b/libs/test/SalesforceHybridTest/res/xml/config.xml index aacebdc525..b10009f64f 100644 --- a/libs/test/SalesforceHybridTest/res/xml/config.xml +++ b/libs/test/SalesforceHybridTest/res/xml/config.xml @@ -1,7 +1,7 @@ + version = "11.1.0"> diff --git a/package.json b/package.json index b1c5a59f98..07f45a45b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SalesforceMobileSDK-Android", - "version": "11.0.0", + "version": "11.1.0", "description": "Salesforce Mobile SDK for Android", "keywords": [ "mobilesdk", diff --git a/tools/generate_doc.sh b/tools/generate_doc.sh index ad5d194cfb..799de34c22 100755 --- a/tools/generate_doc.sh +++ b/tools/generate_doc.sh @@ -3,5 +3,5 @@ if [ ! -d "external" ] then echo "You must run this tool from the root directory of your repo clone" else - javadoc -d doc -author -version -verbose -use -doctitle "SalesforceSDK 11.0 API" -sourcepath "libs/SalesforceAnalytics/src:libs/SalesforceSDK/src:libs/SmartStore/src:libs/MobileSync/src:libs/SalesforceHybrid/src:libs/SalesforceReact/src" -subpackages com + javadoc -d doc -author -version -verbose -use -doctitle "SalesforceSDK 11.1 API" -sourcepath "libs/SalesforceAnalytics/src:libs/SalesforceSDK/src:libs/SmartStore/src:libs/MobileSync/src:libs/SalesforceHybrid/src:libs/SalesforceReact/src" -subpackages com fi From 531a2e56cb3175a03ca9eaa55cc06617ba493ef2 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 23 Jun 2023 15:07:09 -0700 Subject: [PATCH 02/24] Update Maven Central archive classifier and team members. --- publish/publish-module.gradle | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/publish/publish-module.gradle b/publish/publish-module.gradle index b98bdd55cf..81c2497b7a 100644 --- a/publish/publish-module.gradle +++ b/publish/publish-module.gradle @@ -9,9 +9,9 @@ android { } } -task sourcesJar(type: Jar) { +tasks.register('sourcesJar', Jar) { from android.sourceSets.main.java.srcDirs - archiveClassifier.set('sources') + archiveClassifier.set(name + '-sources') } artifacts { @@ -52,9 +52,14 @@ afterEvaluate { email = 'wmathurin@salesforce.com ' } developer { - id = 'bhariharan' - name = 'Bharath Hariharan' - email = 'bhariharan@salesforce.com' + id = 'brianna.birman' + name = 'Brianna Birman' + email = 'brianna.birman@salesforce.com' + } + developer { + id = 'johnson.eric' + name = 'Eric Johnson' + email = 'johnson.eric@salesforce.com' } } scm { From 637ac218c5f67194a59cf65ef24a1d4fc28b623d Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 23 Jun 2023 16:39:46 -0700 Subject: [PATCH 03/24] Remove archiveClassifier for Maven Central publish. --- publish/publish-module.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/publish/publish-module.gradle b/publish/publish-module.gradle index 81c2497b7a..66b833af05 100644 --- a/publish/publish-module.gradle +++ b/publish/publish-module.gradle @@ -11,7 +11,6 @@ android { tasks.register('sourcesJar', Jar) { from android.sourceSets.main.java.srcDirs - archiveClassifier.set(name + '-sources') } artifacts { From 017434c478f7ed62cde5413751403fd4499b27ff Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Mon, 26 Jun 2023 14:48:22 -0700 Subject: [PATCH 04/24] no message --- publish/publish-module.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish/publish-module.gradle b/publish/publish-module.gradle index 66b833af05..838e76a92f 100644 --- a/publish/publish-module.gradle +++ b/publish/publish-module.gradle @@ -56,7 +56,7 @@ afterEvaluate { email = 'brianna.birman@salesforce.com' } developer { - id = 'johnson.eric' + id = 'JohnsonEricAtSalesforce' name = 'Eric Johnson' email = 'johnson.eric@salesforce.com' } From 5dff993be72a3393594dbb6bc80c93528b91923c Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 26 Jun 2023 15:32:24 -0700 Subject: [PATCH 05/24] Graceful handling of non-existent IDP app During SP initiated login flow, clicking "Login with IDP" will show a toast saying "Failed to send request to IDP app" if IDP is not installed. (Before, the SP app would crash because of the unhandled ActivityNotFoundException) --- libs/SalesforceSDK/res/values/sf__strings.xml | 1 + .../androidsdk/auth/idp/SPManager.kt | 42 +++++++++++-------- .../auth/idp/interfaces/SPManager.kt | 1 + .../auth/idp/IDPSPManagerTestCase.kt | 5 +++ .../androidsdk/auth/idp/SPManagerTest.kt | 15 +++++++ 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/libs/SalesforceSDK/res/values/sf__strings.xml b/libs/SalesforceSDK/res/values/sf__strings.xml index d0af172215..ff9bae3f50 100644 --- a/libs/SalesforceSDK/res/values/sf__strings.xml +++ b/libs/SalesforceSDK/res/values/sf__strings.xml @@ -50,6 +50,7 @@ Select an account to use + Failed to send request to IDP app Login request sent to IDP app IDP app successfully obtained authorization code IDP app failed to obtain authorization code diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/SPManager.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/SPManager.kt index 127c31ae8b..e78de2d94c 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/SPManager.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/SPManager.kt @@ -74,7 +74,6 @@ internal class SPLoginFlow private constructor(context:Context, val onStatusUpda // Send SP login request spManager.sendLoginRequest(context, spToIDPRequest) - onStatusUpdate(Status.LOGIN_REQUEST_SENT_TO_IDP) } } } @@ -184,23 +183,32 @@ internal class SPManager( } fun sendLoginRequest(context: Context, message: SPToIDPRequest) { - if (context is Activity) { - // This is a SP initiated login, we start the IDP auth code activity from the SP app - addToActiveFlowIfApplicable(message) - val intent = message.toIntent().apply { - putExtra(SRC_APP_PACKAGE_NAME_KEY, context.applicationInfo.packageName) - // Intent action needs to be ACTION_VIEW, so passing message action through extras - putExtra(ACTION_KEY, message.action) - action = Intent.ACTION_VIEW - setPackage(idpAppPackageName) - setClassName(idpAppPackageName, IDPAuthCodeActivity::class.java.name) - addCategory(Intent.CATEGORY_DEFAULT) + try { + if (context is Activity) { + // This is a SP initiated login, we start the IDP auth code activity from the SP app + addToActiveFlowIfApplicable(message) + val intent = message.toIntent().apply { + putExtra(SRC_APP_PACKAGE_NAME_KEY, context.applicationInfo.packageName) + // Intent action needs to be ACTION_VIEW, so passing message action through extras + putExtra(ACTION_KEY, message.action) + action = Intent.ACTION_VIEW + setPackage(idpAppPackageName) + setClassName(idpAppPackageName, IDPAuthCodeActivity::class.java.name) + addCategory(Intent.CATEGORY_DEFAULT) + } + startActivity(context, intent) + } else { + // This is a IDP initiated login, we will send the message to the IDP receiver + // and let the IDP app start the IDP auth code activity + send(context, message) + } + (getActiveFlow() as? SPLoginFlow)?.let { + it.onStatusUpdate(Status.LOGIN_REQUEST_SENT_TO_IDP) + } + } catch (e: RuntimeException) { + (getActiveFlow() as? SPLoginFlow)?.let { + it.onStatusUpdate(Status.FAILED_TO_SEND_REQUEST_TO_IDP) } - startActivity(context, intent) - } else { - // This is a IDP initiated login, we will send the message to the IDP receiver - // and let the IDP app start the IDP auth code activity - send(context, message) } } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/interfaces/SPManager.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/interfaces/SPManager.kt index 9f5ba31084..17c63e3c49 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/interfaces/SPManager.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/interfaces/SPManager.kt @@ -33,6 +33,7 @@ import com.salesforce.androidsdk.R interface SPManager { enum class Status(val resIdForDescription:Int) { + FAILED_TO_SEND_REQUEST_TO_IDP(R.string.sf__failed_to_send_request_to_idp), LOGIN_REQUEST_SENT_TO_IDP(R.string.sf__login_request_sent_to_idp), AUTH_CODE_RECEIVED_FROM_IDP(R.string.sf__auth_code_received_from_idp), ERROR_RECEIVED_FROM_IDP(R.string.sf__error_received_from_idp), diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/IDPSPManagerTestCase.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/IDPSPManagerTestCase.kt index 242868d831..3325c92a0e 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/IDPSPManagerTestCase.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/IDPSPManagerTestCase.kt @@ -8,6 +8,7 @@ import androidx.test.platform.app.InstrumentationRegistry import com.salesforce.androidsdk.accounts.UserAccount import com.salesforce.androidsdk.util.LogUtil import org.junit.Assert +import java.lang.RuntimeException import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.BlockingQueue import java.util.concurrent.TimeUnit @@ -39,6 +40,10 @@ internal open class IDPSPManagerTestCase { recordEvent("startActivity ${LogUtil.intentToString(intent)}") } + fun throwOnSend(context: Context, intent: Intent) { + throw RuntimeException() + } + fun waitForEvent(expectedEvent: String) { Log.i(this::class.java.simpleName, "waiting for event [$expectedEvent]") val actualEvent = recordedEvents.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS) diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/SPManagerTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/SPManagerTest.kt index c51f25bd78..5e285c9db7 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/SPManagerTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/SPManagerTest.kt @@ -118,6 +118,21 @@ internal class SPManagerTest : IDPSPManagerTestCase() { simulateIDPToSPResponseFromIDPWithBadCode(spManager, uuid, spInitiated = true) } + @Test + fun testKickOffSPInitiatedLoginFlowWithNotExistentIDP() { + val spManager = SPManager("some-idp", TestSDKMgr(buildUser("some-org-id", "some-user-id")), + // the test context is not an activity so sendBroadcast will be used to send the login request to the IDP app + // so throwing an exception there + this::throwOnSend, this::throwOnSend) + spManager.kickOffSPInitiatedLoginFlow(context, TestStatusUpdateCallback()) + + // Make sure the sp got a status update indicating a non-existent IDP + waitForEvent("status FAILED_TO_SEND_REQUEST_TO_IDP") + + // Make sure there are no more events + expectNoEvent() + } + @Test fun testIDPInitiatedFlowForExistingUser() { // Set up sp manager with a current user From edcf947363f7c49d73a7d6f1031bd32b49beaa8b Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 26 Jun 2023 17:28:20 -0700 Subject: [PATCH 06/24] Not getting user info to refresh access token User info is a /services/oauth2 end point, not /services/data one. It returns a 403 for an invalid access token which we don't handle automatically. See end of that [doc](https://help.salesforce.com/s/articleView?language=en_US&id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5). Initially, we would refresh the access token upon getting a 401 or 403, but certain clients relied on that 403 to determinate if their token was invalid, so we had it controlled by a flag, and then we got rid of the flag but we kept using the calls to user info for our "cheap rest call" to refresh the access token. In this PR, we are adding a wrapper for "limits" (with a test). And we are now using a call to "limits" as our "cheap rest call" (in the IDP code and in SalesforceDroidGapActivity) --- .../notesync/ContentSoqlSyncDownTarget.java | 2 +- .../ui/SalesforceDroidGapActivity.java | 5 +++-- .../androidsdk/auth/idp/IDPAuthCodeHelper.kt | 3 ++- .../androidsdk/rest/RestRequest.java | 18 +++++++++++++----- .../androidsdk/rest/RestClientTest.java | 12 ++++++++++++ 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/hybrid/HybridSampleApps/NoteSync/src/com/salesforce/samples/notesync/ContentSoqlSyncDownTarget.java b/hybrid/HybridSampleApps/NoteSync/src/com/salesforce/samples/notesync/ContentSoqlSyncDownTarget.java index f97171044d..cf4c840dbc 100644 --- a/hybrid/HybridSampleApps/NoteSync/src/com/salesforce/samples/notesync/ContentSoqlSyncDownTarget.java +++ b/hybrid/HybridSampleApps/NoteSync/src/com/salesforce/samples/notesync/ContentSoqlSyncDownTarget.java @@ -97,7 +97,7 @@ public ContentSoqlSyncDownTarget(JSONObject target) throws JSONException { @Override public JSONArray startFetch(SyncManager syncManager, long maxTimeStamp) throws IOException, JSONException { String queryToRun = maxTimeStamp > 0 ? SoqlSyncDownTarget.addFilterForReSync(getQuery(maxTimeStamp), getModificationDateFieldName(), maxTimeStamp) : getQuery(maxTimeStamp); - syncManager.getRestClient().sendSync(RestRequest.getRequestForUserInfo()); // cheap call to refresh session + syncManager.getRestClient().sendSync(RestRequest.getRequestForLimits(ApiVersionStrings.VERSION_NUMBER)); // cheap call to refresh session RestRequest request = buildQueryRequest(syncManager.getRestClient().getAuthToken(), queryToRun); RestResponse response = syncManager.sendSyncWithMobileSyncUserAgent(request); JSONArray records = parseSoapResponse(response); diff --git a/libs/SalesforceHybrid/src/com/salesforce/androidsdk/phonegap/ui/SalesforceDroidGapActivity.java b/libs/SalesforceHybrid/src/com/salesforce/androidsdk/phonegap/ui/SalesforceDroidGapActivity.java index db2ce1cf71..27c5b6c06b 100644 --- a/libs/SalesforceHybrid/src/com/salesforce/androidsdk/phonegap/ui/SalesforceDroidGapActivity.java +++ b/libs/SalesforceHybrid/src/com/salesforce/androidsdk/phonegap/ui/SalesforceDroidGapActivity.java @@ -37,6 +37,7 @@ import com.salesforce.androidsdk.config.LoginServerManager; import com.salesforce.androidsdk.phonegap.app.SalesforceHybridSDKManager; import com.salesforce.androidsdk.phonegap.util.SalesforceHybridLogger; +import com.salesforce.androidsdk.rest.ApiVersionStrings; import com.salesforce.androidsdk.rest.ClientManager; import com.salesforce.androidsdk.rest.ClientManager.AccountInfoNotFoundException; import com.salesforce.androidsdk.rest.ClientManager.RestClientCallback; @@ -349,7 +350,7 @@ public void authenticatedRestClient(RestClient client) { * but a stale session ID will cause the WebView to redirect * to the web login. */ - SalesforceDroidGapActivity.this.client.sendAsync(RestRequest.getRequestForUserInfo(), new AsyncRequestCallback() { + SalesforceDroidGapActivity.this.client.sendAsync(RestRequest.getRequestForLimits(ApiVersionStrings.VERSION_NUMBER), new AsyncRequestCallback() { @Override public void onSuccess(RestRequest request, RestResponse response) { @@ -422,7 +423,7 @@ public void authenticatedRestClient(RestClient client) { }); return; } - client.sendAsync(RestRequest.getRequestForUserInfo(), new AsyncRequestCallback() { + client.sendAsync(RestRequest.getRequestForLimits(ApiVersionStrings.VERSION_NUMBER), new AsyncRequestCallback() { @Override public void onSuccess(RestRequest request, RestResponse response) { diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt index 7204ac1924..98b3459eae 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt @@ -39,6 +39,7 @@ import com.salesforce.androidsdk.app.SalesforceSDKManager import com.salesforce.androidsdk.auth.OAuth2.getAuthorizationUrl import com.salesforce.androidsdk.auth.OAuth2.getFrontdoorUrl import com.salesforce.androidsdk.config.BootConfig +import com.salesforce.androidsdk.rest.ApiVersionStrings import com.salesforce.androidsdk.rest.ClientManager import com.salesforce.androidsdk.rest.RestClient import com.salesforce.androidsdk.rest.RestRequest @@ -119,7 +120,7 @@ internal class IDPAuthCodeHelper private constructor( SalesforceSDKLogger.d(TAG, "Obtaining valid access token") buildRestClient()?.let {restClient -> val restResponse = try { - restClient.sendSync(RestRequest.getRequestForUserInfo()) + restClient.sendSync(RestRequest.getRequestForLimits(ApiVersionStrings.VERSION_NUMBER)) } catch (e: IOException) { SalesforceSDKLogger.e(TAG, "Failed to obtain valid access token", e) null diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestRequest.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestRequest.java index 2247d21629..de999a03f1 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestRequest.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestRequest.java @@ -29,13 +29,10 @@ import android.net.Uri; import android.text.TextUtils; -import com.salesforce.androidsdk.app.SalesforceSDKManager; -import com.salesforce.androidsdk.config.BootConfig; import com.salesforce.androidsdk.rest.BatchRequest.BatchRequestBuilder; import com.salesforce.androidsdk.rest.CompositeRequest.CompositeRequestBuilder; import com.salesforce.androidsdk.rest.files.ConnectUriBuilder; import com.salesforce.androidsdk.util.JSONObjectHelper; -import com.salesforce.androidsdk.util.SalesforceSDKLogger; import org.json.JSONArray; import org.json.JSONException; @@ -55,7 +52,6 @@ import java.util.Map; import java.util.TimeZone; -import okhttp3.FormBody; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -189,7 +185,8 @@ enum RestAction { SOBJECT_COLLECTION_UPSERT(SERVICES_DATA + "%s/composite/sobjects/%s/%s"), NOTIFICATIONS_STATUS(SERVICES_DATA + "%s/connect/notifications/status"), NOTIFICATIONS(SERVICES_DATA + "%s/connect/notifications/%s"), - PRIMING_RECORDS(SERVICES_DATA + "%s/connect/briefcase/priming-records"); + PRIMING_RECORDS(SERVICES_DATA + "%s/connect/briefcase/priming-records"), + LIMITS(SERVICES_DATA + "%s/limits"); private final String pathTemplate; @@ -949,6 +946,17 @@ public static RestRequest getRequestForCollectionDelete(String apiVersion, boole return new RestRequest(RestMethod.DELETE, path.toString()); } + /** + * Request for getting information about limits in your org + * + * @param apiVersion Salesforce API version. + * @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_limits.htm + */ + public static RestRequest getRequestForLimits(String apiVersion) { + StringBuilder path = new StringBuilder(RestAction.LIMITS.getPath(apiVersion)); + return new RestRequest(RestMethod.GET, path.toString()); + } + /** * Helper method for creating conditional HTTP header. * diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/rest/RestClientTest.java b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/rest/RestClientTest.java index 015f43603b..d75d1ecec2 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/rest/RestClientTest.java +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/rest/RestClientTest.java @@ -1445,6 +1445,18 @@ public void testCollectionCreateWithBadRecordAndAllOrNoneTrue() throws JSONExcep Assert.assertEquals("ALL_OR_NONE_OPERATION_ROLLED_BACK", parsedResponse.subResponses.get(1).errors.get(0).statusCode); } + /** + * Testing a limits call to the server - check response + * @throws Exception + */ + @Test + public void testLimits() throws Exception { + RestResponse response = restClient.sendSync(RestRequest.getRequestForLimits(TestCredentials.API_VERSION)); + checkResponse(response, HttpURLConnection.HTTP_OK, false); + JSONObject jsonResponse = response.asJSONObject(); + checkKeys(jsonResponse, "DailyApiRequests"); + } + // // Helper methods // From c410aecf997b835faf16d02262a40a26fe766eb5 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Wed, 28 Jun 2023 14:12:33 -0700 Subject: [PATCH 07/24] New tests where we query soup elements with non-ascii characters in string indexed, json1 indexed and non-indexed fields --- .../smartstore/store/SmartSqlTest.java | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/SmartSqlTest.java b/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/SmartSqlTest.java index 005845332c..087846ea06 100644 --- a/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/SmartSqlTest.java +++ b/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/SmartSqlTest.java @@ -396,7 +396,7 @@ public void testSmartQueryMatchingNullField() throws JSONException { } /** - * Test running smart queries that matching true and false in json1 field + * Test running smart queries matching true and false in json1 field * NB: SQLite does not have a separate Boolean storage class. Instead, Boolean values are stored as integers 0 (false) and 1 (true). */ @Test @@ -421,6 +421,69 @@ public void testSmartQueryMachingBooleanInJSON1Field() throws JSONException { JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"00020\"],[\"00060\"],[\"00070\"],[\"00310\"],[\"102\"]]"), result); } + /** + * Test running smart queries matching non-ASCII characters in String field + */ + @Test + public void testSmartQueryMachingNonAsciiInStringField() throws JSONException { + + // Creating another employee from a json string with first name using non-ascii characters (Turkish) + createEmployeeWithJsonString("{\"employeeId\":\"101\",\"firstName\":\"Göktuğ\"}"); + + // Creating another employee from a json string with first name using non-ascii characters (Korean) + createEmployeeWithJsonString("{\"employeeId\":\"102\",\"firstName\":\"보배\"}"); + + // Smart sql looking for first name containing a certain non-ASCII character (ğ) + JSONArray result = store.query(QuerySpec.buildSmartQuerySpec("select {employees:employeeId} from {employees} where {employees:firstName} like '%ğ%'", 10), 0); + JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"101\"]]"), result); + + // Smart sql looking for first name containing a certain non-ASCII character (배) + result = store.query(QuerySpec.buildSmartQuerySpec("select {employees:employeeId} from {employees} where {employees:firstName} like '%배%'", 10), 0); + JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"102\"]]"), result); + } + + /** + * Test running smart queries matching non-ASCII characters in Json1 field + */ + @Test + public void testSmartQueryMachingNonAsciiInJSON1Field() throws JSONException { + + // Creating another employee from a json string with education using non-ascii characters (Turkish) + createEmployeeWithJsonString("{\"employeeId\":\"101\",\"education\":\"latince uzmanı\"}"); + + // Creating another employee from a json string with education using non-ascii characters (Korean) + createEmployeeWithJsonString("{\"employeeId\":\"102\",\"education\":\"라틴어 전문가\"}"); + + // Smart sql looking for education containing a certain non-ASCII character (ı) + JSONArray result = store.query(QuerySpec.buildSmartQuerySpec("select {employees:employeeId} from {employees} where {employees:education} like '%ı%'", 10), 0); + JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"101\"]]"), result); + + // Smart sql looking for education containing a certain non-ASCII character (문) + result = store.query(QuerySpec.buildSmartQuerySpec("select {employees:employeeId} from {employees} where {employees:education} like '%문%'", 10), 0); + JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"102\"]]"), result); + } + + /** + * Test running smart queries matching non-ASCII characters in non-indexed field + */ + @Test + public void testSmartQueryMachingNonAsciiInNonIndexedField() throws JSONException { + + // Creating another employee from a json string with country using non-ascii characters (Turkish) + createEmployeeWithJsonString("{\"employeeId\":\"101\",\"country\":\"Türkçe\"}"); + + // Creating another employee from a json string with country using non-ascii characters (Korean) + createEmployeeWithJsonString("{\"employeeId\":\"102\",\"country\":\"한국\"}"); + + // Smart sql looking for country containing a certain non-ASCII character (ç) + JSONArray result = store.query(QuerySpec.buildSmartQuerySpec("select {employees:employeeId} from {employees} where {employees:country} like '%ç%'", 10), 0); + JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"101\"]]"), result); + + // Smart sql looking for country containing a certain non-ASCII character (국) + result = store.query(QuerySpec.buildSmartQuerySpec("select {employees:employeeId} from {employees} where {employees:country} like '%국%'", 10), 0); + JSONTestHelper.assertSameJSONArray("Wrong result", new JSONArray("[[\"102\"]]"), result); + } + @Test public void testSmartQueryFilteringByNonIndexedField() throws JSONException { JSONObject employee101 = createEmployeeWithJsonString("{\"employeeId\":\"101\",\"address\":{\"city\":\"San Francisco\", \"zipcode\":94105}}"); From 2541cbdfbe024a2989205899e23eb2d129bf8973 Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Thu, 29 Jun 2023 15:59:10 -0600 Subject: [PATCH 08/24] @W-12610994: [Android] Update reference to SHA1PRNG algorithm (#2432) --- .../analytics/security/Encryptor.java | 9 ++++----- .../security/SalesforceKeyGenerator.java | 10 ++-------- .../analytics/security/EncryptorTest.java | 19 +++++++++---------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/libs/SalesforceAnalytics/src/com/salesforce/androidsdk/analytics/security/Encryptor.java b/libs/SalesforceAnalytics/src/com/salesforce/androidsdk/analytics/security/Encryptor.java index 814fe4a741..534d1f472a 100644 --- a/libs/SalesforceAnalytics/src/com/salesforce/androidsdk/analytics/security/Encryptor.java +++ b/libs/SalesforceAnalytics/src/com/salesforce/androidsdk/analytics/security/Encryptor.java @@ -41,7 +41,6 @@ import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; @@ -60,7 +59,6 @@ public class Encryptor { private static final String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding"; private static final String AES_GCM_CIPHER = "AES/GCM/NoPadding"; private static final String MAC_TRANSFORMATION = "HmacSHA256"; - private static final String SHA1PRNG = "SHA1PRNG"; private static final String RSA_PKCS1 = "RSA/ECB/PKCS1Padding"; private static final String BOUNCY_CASTLE = "BC"; private static final int READ_BUFFER_LENGTH = 1024; @@ -72,7 +70,7 @@ public class Encryptor { * @return Initialized cipher. */ public static Cipher getEncryptingCipher(String encryptionKey) - throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { + throws InvalidAlgorithmParameterException, InvalidKeyException { final byte[] keyBytes = Base64.decode(encryptionKey, Base64.DEFAULT); return getEncryptingCipher(keyBytes, generateInitVector()); } @@ -534,8 +532,9 @@ private static byte[] decryptWithPrivateKey(PrivateKey privateKey, String data, return null; } - private static byte[] generateInitVector() throws NoSuchAlgorithmException { - final SecureRandom random = SecureRandom.getInstance(SHA1PRNG); + private static byte[] generateInitVector() { + // Create the system recommended secure random number generator provider algorithm. + final SecureRandom random = new SecureRandom(); byte[] iv = new byte[12]; random.nextBytes(iv); return iv; diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java index 8adf563e38..05e2d74a24 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java @@ -60,9 +60,7 @@ public class SalesforceKeyGenerator { private static final String ENCRYPTED_ID_SHARED_PREF_KEY = "encrypted_%s"; private static final String ID_PREFIX = "id_"; private static final String KEYSTORE_ALIAS = "com.salesforce.androidsdk.security.KEYPAIR"; - private static final String SHA1 = "SHA-1"; private static final String SHA256 = "SHA-256"; - private static final String SHA1PRNG = "SHA1PRNG"; private static final String AES = "AES"; private static final Map CACHED_ENCRYPTION_KEYS = new ConcurrentHashMap<>(); @@ -165,13 +163,9 @@ private synchronized static String generateUniqueId(String name, int length) { } else { String uniqueId; try { - - // Uses SecureRandom to generate an AES-256 key. - final SecureRandom secureRandom = SecureRandom.getInstance(SHA1PRNG); - - // SecureRandom does not require seeding. It's automatically seeded from system entropy. + // Create the key generator with its recommended secure random number generator provider algorithm. final KeyGenerator keyGenerator = KeyGenerator.getInstance(AES); - keyGenerator.init(length, secureRandom); + keyGenerator.init(length); // Generates a 256-bit key. uniqueId = Base64.encodeToString(keyGenerator.generateKey().getEncoded(), Base64.NO_WRAP); diff --git a/libs/test/SalesforceAnalyticsTest/src/com/salesforce/androidsdk/analytics/security/EncryptorTest.java b/libs/test/SalesforceAnalyticsTest/src/com/salesforce/androidsdk/analytics/security/EncryptorTest.java index c37f515ec4..14746fad37 100644 --- a/libs/test/SalesforceAnalyticsTest/src/com/salesforce/androidsdk/analytics/security/EncryptorTest.java +++ b/libs/test/SalesforceAnalyticsTest/src/com/salesforce/androidsdk/analytics/security/EncryptorTest.java @@ -36,7 +36,6 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -71,7 +70,7 @@ public void testEncryptWithNullKey() { Encryptor.encrypt(data, null)); } } - + /** * Test to make sure that decrypt does nothing when given a null key. */ @@ -114,12 +113,12 @@ public void testEncryptDecryptWithDifferentData() { final String decryptedA = Encryptor.decrypt(encryptedA, key); final String encryptedB = Encryptor.encrypt(otherData, key); final String decryptedB = Encryptor.decrypt(encryptedB, key); - boolean sameDecrypted = decryptedA.equals(decryptedB); + boolean sameDecrypted = decryptedA.equals(decryptedB); boolean sameData = data.equals(otherData); Assert.assertEquals("Decrypted strings '" - + decryptedA + "','" + decryptedB + + decryptedA + "','" + decryptedB + "' should be different for different strings '" - + data +"','" + otherData + "'", + + data +"','" + otherData + "'", sameDecrypted, sameData); } } @@ -144,9 +143,9 @@ public void testEncryptDecryptWithDifferentKeys() { Assert.assertEquals("Decrypted values should be the same", decryptedA, decryptedB); boolean sameEncrypted = encryptedA.equals(encryptedB); Assert.assertEquals("Encrypted strings '" - + encryptedA + "','" + encryptedB + + encryptedA + "','" + encryptedB + "' should be different for different keys '" - + key +"','" + otherKey + "'", + + key +"','" + otherKey + "'", sameEncrypted, sameKey); } } @@ -158,7 +157,7 @@ public void testEncryptDecryptWithDifferentKeys() { */ @Test public void testGetEncryptingCipher() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException { + throws InvalidAlgorithmParameterException, InvalidKeyException { Cipher cipher = Encryptor.getEncryptingCipher(makeKey("my-key")); Assert.assertEquals("Wrong algorithm", "AES/GCM/NoPadding", cipher.getAlgorithm()); Assert.assertEquals("Wrong iv length", 12, cipher.getIV().length); @@ -170,7 +169,7 @@ public void testGetEncryptingCipher() */ @Test public void testGetDecryptingCipher() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException { + throws InvalidAlgorithmParameterException, InvalidKeyException { Cipher cipher = Encryptor.getDecryptingCipher(makeKey("my-key"), new byte[12]); Assert.assertEquals("Wrong algorithm", "AES/GCM/NoPadding", cipher.getAlgorithm()); Assert.assertEquals("Wrong iv length", 12, cipher.getIV().length); @@ -183,7 +182,7 @@ public void testGetDecryptingCipher() */ @Test public void testEncryptDecryptWithCipher() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, + throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { String key = makeKey("test-key"); String originalText = "abcdefghijklmnopqrstuvwxyz"; From 88680a1f0cbfce649deb8d3c63ce2b118f5920bf Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Wed, 5 Jul 2023 16:37:03 -0700 Subject: [PATCH 09/24] Update Biometric/IDP login/log in strings. --- libs/SalesforceSDK/res/values/sf__strings.xml | 10 +++++----- .../com/salesforce/androidsdk/ui/LoginActivity.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/SalesforceSDK/res/values/sf__strings.xml b/libs/SalesforceSDK/res/values/sf__strings.xml index ff9bae3f50..74f70f1072 100644 --- a/libs/SalesforceSDK/res/values/sf__strings.xml +++ b/libs/SalesforceSDK/res/values/sf__strings.xml @@ -24,8 +24,8 @@ Change Server Clear Cookies Reload - Login with IDP App - Login with Biometric + Log In with IDP App + Log In with Biometric Setup Biometric Unlock @@ -55,7 +55,7 @@ IDP app successfully obtained authorization code IDP app failed to obtain authorization code Failed to exchange authorization code - Login complete + Log in complete Login request sent to SP app @@ -63,7 +63,7 @@ Failed to get authorization code for SP app Authorization code sent to SP app SP app failed to exchange authorization code - SP app login complete + SP app log in complete Used to specify login hosts for the application. @@ -91,7 +91,7 @@ Biometric Login - Use your fingerprint to quickly login. Biometric information is never stored. + Use your fingerprint to quickly log in. Biometric information is never stored. Enable Use Password Signing out %s. diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.java index cc6db3b9e8..4d0e13dab7 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.java @@ -593,7 +593,7 @@ private BiometricPrompt.PromptInfo getPromptInfo() { String subtitle = SalesforceSDKManager.getInstance().getUserAccountManager() .getCurrentUser().getUsername(); return new BiometricPrompt.PromptInfo.Builder() - .setTitle("Login with Biometric") + .setTitle(getResources().getString(R.string.sf__biometric_opt_in_title)) .setSubtitle(subtitle) .setAllowedAuthenticators(getAuthenticators()) .setConfirmationRequired(hasFaceUnlock) From 62fe812794edbc571e21217eea0ed23b7dae5687 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 7 Jul 2023 11:27:39 -0700 Subject: [PATCH 10/24] Switch to using Restricted Context in CircleCI. --- .circleci/config.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 576c38b2da..27805520a3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ aliases: # Docker image tags can be found here: https://circleci.com/developer/images/image/cimg/android - - &cimg cimg/android:2022.09.2-node + - &cimg cimg/android:2023.06.1-node # Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide - &default-api-level 30 @@ -48,7 +48,7 @@ aliases: command: | sudo npm i npm@latest -g sudo npm install -g shelljs@0.8.5 - sudo npm install -g cordova@8.1.2 + sudo npm install -g cordova sudo npm install -g typescript cordova telemetry off ./install.sh @@ -377,6 +377,7 @@ workflows: - static-analysis - run-tests: name: << matrix.lib >> + context: Android Unit Tests matrix: parameters: lib: ["SalesforceAnalytics", "SalesforceSDK", "SmartStore", "MobileSync", "SalesforceHybrid", "SalesforceReact"] @@ -401,12 +402,14 @@ workflows: jobs: - run-tests: name: << matrix.lib >> API << pipeline.parameters.api-level >> + context: Android Unit Tests matrix: parameters: lib: ["SalesforceAnalytics", "SalesforceSDK", "SmartStore", "MobileSync", "SalesforceHybrid", "SalesforceReact"] api_level: [<< pipeline.parameters.api-level >>] - test-rest-explorer: name: RestExplorer API << pipeline.parameters.api-level >> + context: Android Unit Tests matrix: parameters: api_level: [<< pipeline.parameters.api-level >>] From 85b536dde8d1c74fb6913f5e4e14e5aa8cce0857 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 7 Jul 2023 12:05:02 -0700 Subject: [PATCH 11/24] no message --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 27805520a3..dde85f1ef1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,7 +48,7 @@ aliases: command: | sudo npm i npm@latest -g sudo npm install -g shelljs@0.8.5 - sudo npm install -g cordova + sudo npm install -g cordova@12.0.0 sudo npm install -g typescript cordova telemetry off ./install.sh From d6455d1af9876cd7e275ea7e03ee9f6a1ab12407 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 7 Jul 2023 14:17:32 -0700 Subject: [PATCH 12/24] Fix static analysis CI step. --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dde85f1ef1..4f5fca2935 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -374,7 +374,8 @@ workflows: pattern: "^pull/\\d+$" value: << pipeline.git.branch >> jobs: - - static-analysis + - static-analysis: + context: Android Unit Tests - run-tests: name: << matrix.lib >> context: Android Unit Tests From e70a6a32596adc136cae0757fa721d0a00bda59f Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 27 Jul 2023 11:31:54 -0600 Subject: [PATCH 13/24] Reading stream using a buffer in key value store --- .../store/KeyValueEncryptedFileStore.java | 8 +---- .../store/KeyValueEncryptedFileStoreTest.java | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java b/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java index 722462e723..482ffbd22a 100644 --- a/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java +++ b/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java @@ -463,13 +463,7 @@ void encryptStringToFile(File file, String content, String encryptionKey) throws } void encryptStreamToFile(File file, InputStream stream, String encryptionKey) throws IOException { - final ByteArrayOutputStream b = new ByteArrayOutputStream(); - int nextByte = stream.read(); - while (nextByte != -1) { - b.write(nextByte); - nextByte = stream.read(); - } - byte[] content = b.toByteArray(); + byte[] content = Encryptor.getByteArrayStreamFromStream(stream).toByteArray(); encryptBytesToFile(file, content, encryptionKey); } diff --git a/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java b/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java index ac48ebdeab..a3f8cde0db 100644 --- a/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java +++ b/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java @@ -53,6 +53,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Random; @RunWith(AndroidJUnit4.class) public class KeyValueEncryptedFileStoreTest { @@ -251,6 +253,8 @@ public void testSaveValueGetValue() { } } + + /** Test saving from streams and getting them back as values */ @Test public void testSaveStreamGetValue() { @@ -267,6 +271,32 @@ public void testSaveStreamGetValue() { } } + @Test + public void testSaveStreamGetLargeValue() { + for (int i = 0; i < 24; i++) { + String key = "key" + i; + String value = getLargeString((int) Math.pow(2, i)); + InputStream stream = stringToStream(value); + keyValueStore.saveStream(key, stream); + Assert.assertEquals( + "Wrong value for key: " + key, value, keyValueStore.getValue(key)); + } + } + + private String getLargeString(int size) { + final String CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Random random = new Random(); + StringBuilder sb = new StringBuilder(size); + + for (int i = 0; i < size; i++) { + int randomIndex = random.nextInt(CHARACTERS.length()); + char randomChar = CHARACTERS.charAt(randomIndex); + sb.append(randomChar); + } + + return sb.toString(); + } + /** Test saving values and getting them back as streams */ @Test public void testSaveValueGetStream() { From 87bc25e744074ac6eb03705f051d409cedc4876e Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 27 Jul 2023 11:34:37 -0600 Subject: [PATCH 14/24] Cleaning up imports --- .../androidsdk/smartstore/store/KeyValueEncryptedFileStore.java | 1 - .../smartstore/store/KeyValueEncryptedFileStoreTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java b/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java index 482ffbd22a..6e86679fa4 100644 --- a/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java +++ b/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java @@ -36,7 +36,6 @@ import com.salesforce.androidsdk.util.ManagedFilesHelper; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; diff --git a/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java b/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java index a3f8cde0db..3eb8a4f4eb 100644 --- a/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java +++ b/libs/test/SmartStoreTest/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStoreTest.java @@ -53,7 +53,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Random; @RunWith(AndroidJUnit4.class) From 8e9a6c2f441ad639ba3524c58ea845a654fd7872 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Thu, 3 Aug 2023 15:54:56 -0700 Subject: [PATCH 15/24] Fix handling of my domain changes affecting token refresh. --- .../salesforce/androidsdk/rest/ClientManager.java | 10 ++++++++-- .../com/salesforce/androidsdk/rest/RestClient.java | 13 +++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java index 4d10f83e7e..dc979e1de7 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java @@ -653,12 +653,18 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { final AccountManager mgr = AccountManager.get(context); final String encryptionKey = SalesforceSDKManager.getEncryptionKey(); final String refreshToken = SalesforceSDKManager.decrypt(mgr.getPassword(account), encryptionKey); - final String loginServer = SalesforceSDKManager.decrypt(mgr.getUserData(account, - AuthenticatorService.KEY_LOGIN_URL), encryptionKey); final String clientId = SalesforceSDKManager.decrypt(mgr.getUserData(account, AuthenticatorService.KEY_CLIENT_ID), encryptionKey); final String instServer = SalesforceSDKManager.decrypt(mgr.getUserData(account, AuthenticatorService.KEY_INSTANCE_URL), encryptionKey); + final String idUrl = SalesforceSDKManager.decrypt(mgr.getUserData(account, + AuthenticatorService.KEY_ID_URL), encryptionKey); + /* + * The login server we store is the actual url the user originally used to login, which + * could be a my domain that changes in the future. The id url always points to the + * server the user exists on so it should always exist. + */ + final String loginServer = idUrl.split("/id/")[0]; final List additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys(); Map values = null; if (additionalOauthKeys != null && !additionalOauthKeys.isEmpty()) { diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestClient.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestClient.java index f4e6685af8..e38a7ebfd7 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestClient.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/RestClient.java @@ -710,9 +710,18 @@ public Response intercept(Chain chain) throws IOException { * only if the host of the request was the old instance URL. This avoids * accidental manipulation of the host for requests where the caller has * passed in their own fully formed host URL that is not instance URL. + * + * We also need to cover the case where the host changes during refresh + * because the replayed request will fail. */ - if (isHostInstanceUrl && !currentInstanceUrl.host().equals(request.url().host())) { - request = adjustHostInRequest(request, currentInstanceUrl.host()); + final URI refreshInstanceUrl = clientInfo.getInstanceUrl(); + boolean refreshUpdatedUrl = refreshInstanceUrl != null && + !refreshInstanceUrl.getHost().equals(request.url().host()); + if (isHostInstanceUrl && refreshUpdatedUrl) { + final HttpUrl updatedInstanceUrl = HttpUrl.get(refreshInstanceUrl); + if (updatedInstanceUrl != null) { + request = adjustHostInRequest(request, updatedInstanceUrl.host()); + } } response.close(); response = chain.proceed(request); From 93d877264a98f664edf0433710faeebd740b0f50 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Tue, 8 Aug 2023 18:02:40 -0700 Subject: [PATCH 16/24] Use community or instance url for token refresh. --- .../salesforce/androidsdk/rest/ClientManager.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java index dc979e1de7..55176f12c2 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java @@ -657,14 +657,10 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { AuthenticatorService.KEY_CLIENT_ID), encryptionKey); final String instServer = SalesforceSDKManager.decrypt(mgr.getUserData(account, AuthenticatorService.KEY_INSTANCE_URL), encryptionKey); - final String idUrl = SalesforceSDKManager.decrypt(mgr.getUserData(account, - AuthenticatorService.KEY_ID_URL), encryptionKey); - /* - * The login server we store is the actual url the user originally used to login, which - * could be a my domain that changes in the future. The id url always points to the - * server the user exists on so it should always exist. - */ - final String loginServer = idUrl.split("/id/")[0]; + final String communityUrl = SalesforceSDKManager.decrypt(mgr.getUserData(account, + AuthenticatorService.KEY_COMMUNITY_URL), encryptionKey); + + final String url = (communityUrl != null) ? communityUrl : instServer; final List additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys(); Map values = null; if (additionalOauthKeys != null && !additionalOauthKeys.isEmpty()) { @@ -680,7 +676,7 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { final Map addlParamsMap = SalesforceSDKManager.getInstance().getLoginOptions().getAdditionalParameters(); try { final OAuth2.TokenEndpointResponse tr = OAuth2.refreshAuthToken(HttpAccess.DEFAULT, - new URI(loginServer), clientId, refreshToken, addlParamsMap); + new URI(url), clientId, refreshToken, addlParamsMap); if (!instServer.equalsIgnoreCase(tr.instanceUrl)) { mgr.setUserData(account, AuthenticatorService.KEY_INSTANCE_URL, SalesforceSDKManager.encrypt(tr.instanceUrl, encryptionKey)); From f2c71e755f208fa8864b34e18438b9facf67fdec Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Thu, 10 Aug 2023 14:29:04 -0700 Subject: [PATCH 17/24] Fully support use custom domain button in webview. --- .../androidsdk/ui/OAuthWebviewHelper.java | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index 4893e18ffc..2c7d612fc2 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java @@ -80,6 +80,7 @@ import com.salesforce.androidsdk.security.BiometricAuthenticationManager; import com.salesforce.androidsdk.security.SalesforceKeyGenerator; import com.salesforce.androidsdk.security.ScreenLockManager; +import com.salesforce.androidsdk.util.AuthConfigTask; import com.salesforce.androidsdk.util.EventsObservable; import com.salesforce.androidsdk.util.EventsObservable.EventType; import com.salesforce.androidsdk.util.MapUtil; @@ -473,7 +474,7 @@ protected String getLoginUrl() { * WebViewClient which intercepts the redirect to the oauth callback url. * That redirect marks the end of the user facing portion of the authentication flow. */ - protected class AuthWebViewClient extends WebViewClient { + protected class AuthWebViewClient extends WebViewClient implements AuthConfigTask.AuthConfigCallbackInterface { @Override public void onPageFinished(WebView view, String url) { @@ -500,6 +501,34 @@ public void onPageFinished(WebView view, String url) { } } } + + // Check if user entered a custom domain + if (!url.contains(getLoginUrl())) { + try { + URI newLoginUrl = new URI(url); + String baseUrl = "https://" + newLoginUrl.getHost(); + LoginServerManager serverManager = SalesforceSDKManager.getInstance().getLoginServerManager(); + LoginServerManager.LoginServer loginServer = serverManager.getLoginServerFromURL(baseUrl); + + if (isSalesforceUrl(baseUrl)) { + // Check if url is already in server list + if (loginServer == null) { + // Add also sets as selected + serverManager.addCustomLoginServer("Custom Domain", baseUrl); + } else { + serverManager.setSelectedLoginServer(loginServer); + } + + // Set title to new login url + loginOptions.setLoginUrl(baseUrl); + // Checks the config for the selected login server + (new AuthConfigTask(this)).execute(); + } + } catch (Exception e) { + SalesforceSDKLogger.e(TAG, "Unable to retrieve auth config."); + } + } + EventsObservable.get().notifyEvent(EventType.AuthWebViewPageFinished, url); super.onPageFinished(view, url); } @@ -522,8 +551,10 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request return true; } - boolean isDone = uri.toString().replace("///", "/").toLowerCase(Locale.US).startsWith(loginOptions.getOauthCallbackUrl().replace("///", "/").toLowerCase(Locale.US)); - if (isDone) { + String formattedUrl = uri.toString().replace("///", "/").toLowerCase(Locale.US); + String callbackUrl = loginOptions.getOauthCallbackUrl().replace("///", "/").toLowerCase(Locale.US); + boolean authFlowFinished = formattedUrl.startsWith(callbackUrl); + if (authFlowFinished) { Map params = UriFragmentParser.parse(uri); String error = params.get("error"); // Did we fail? @@ -541,8 +572,27 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request onAuthFlowComplete(tr); } } + } else { + // Only allow redirect for requests with authorization headers for Salesforce urls. + if (request.getRequestHeaders() != null && request.getRequestHeaders().containsKey("Authorization")) { + return isSalesforceUrl(formattedUrl); + } } - return isDone; + + return authFlowFinished; + } + + // List from https://help.salesforce.com/s/articleView?language=en_US&id=sf.domain_name_url_formats.htm&type=5 + private boolean isSalesforceUrl(String host) { + return (host != null && (host.endsWith(".salesforce.com") || + host.endsWith(".force.com") || + host.endsWith(".sfdcopens.com") || + host.endsWith(".site.com") || + host.endsWith(".lightning.com") || + host.endsWith(".salesforce-sites.com") || + host.endsWith(".force-user-content.com") || + host.endsWith(".salesforce-experience.com") || + host.endsWith(".salesforce-scrt.com"))); } @Override @@ -570,6 +620,15 @@ public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) SalesforceSDKLogger.d(TAG, "Received client certificate request from server"); request.proceed(key, certChain); } + + @Override + public void onAuthConfigFetched() { + SalesforceSDKManager manager = SalesforceSDKManager.getInstance(); + if (manager.isBrowserLoginEnabled()) { + // This load will trigger advanced auth and do all necessary setup. + doLoadPage(); + } + } } /** From 87b749ff500894c94ec45dc08a4dee653a09590a Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Fri, 11 Aug 2023 14:01:24 -0700 Subject: [PATCH 18/24] Update method of determining new login url. --- .../androidsdk/ui/OAuthWebviewHelper.java | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index 2c7d612fc2..9d4a02502e 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java @@ -502,33 +502,6 @@ public void onPageFinished(WebView view, String url) { } } - // Check if user entered a custom domain - if (!url.contains(getLoginUrl())) { - try { - URI newLoginUrl = new URI(url); - String baseUrl = "https://" + newLoginUrl.getHost(); - LoginServerManager serverManager = SalesforceSDKManager.getInstance().getLoginServerManager(); - LoginServerManager.LoginServer loginServer = serverManager.getLoginServerFromURL(baseUrl); - - if (isSalesforceUrl(baseUrl)) { - // Check if url is already in server list - if (loginServer == null) { - // Add also sets as selected - serverManager.addCustomLoginServer("Custom Domain", baseUrl); - } else { - serverManager.setSelectedLoginServer(loginServer); - } - - // Set title to new login url - loginOptions.setLoginUrl(baseUrl); - // Checks the config for the selected login server - (new AuthConfigTask(this)).execute(); - } - } catch (Exception e) { - SalesforceSDKLogger.e(TAG, "Unable to retrieve auth config."); - } - } - EventsObservable.get().notifyEvent(EventType.AuthWebViewPageFinished, url); super.onPageFinished(view, url); } @@ -551,6 +524,30 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request return true; } + // Check if user entered a custom domain + if (isNewLoginUrl(uri)) { + try { + String baseUrl = "https://" + uri.getHost(); + LoginServerManager serverManager = SalesforceSDKManager.getInstance().getLoginServerManager(); + LoginServerManager.LoginServer loginServer = serverManager.getLoginServerFromURL(baseUrl); + + // Check if url is already in server list + if (loginServer == null) { + // Add also sets as selected + serverManager.addCustomLoginServer("Custom Domain", baseUrl); + } else { + serverManager.setSelectedLoginServer(loginServer); + } + + // Set title to new login url + loginOptions.setLoginUrl(baseUrl); + // Checks the config for the selected login server + (new AuthConfigTask(this)).execute(); + } catch (Exception e) { + SalesforceSDKLogger.e(TAG, "Unable to retrieve auth config."); + } + } + String formattedUrl = uri.toString().replace("///", "/").toLowerCase(Locale.US); String callbackUrl = loginOptions.getOauthCallbackUrl().replace("///", "/").toLowerCase(Locale.US); boolean authFlowFinished = formattedUrl.startsWith(callbackUrl); @@ -595,6 +592,16 @@ private boolean isSalesforceUrl(String host) { host.endsWith(".salesforce-scrt.com"))); } + private boolean isNewLoginUrl(Uri uri) { + if (!uri.toString().contains(getLoginUrl()) && isSalesforceUrl(uri.getHost())) { + String path = uri.getQuery(); + return path != null && + path.startsWith("startURL=/setup/secur/RemoteAccessAuthorizationPage.apexp?source="); + } + + return false; + } + @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { int primError = error.getPrimaryError(); From 92c1252dea7b500d0d4db37e59eea8706eacee1a Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Mon, 14 Aug 2023 16:26:27 -0700 Subject: [PATCH 19/24] Add flag to disable the check for custom domain. --- .../androidsdk/app/SalesforceSDKManager.java | 20 +++++++++++++++++++ .../androidsdk/ui/OAuthWebviewHelper.java | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java index 2fc7ef9d56..3c04cba788 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java @@ -181,6 +181,9 @@ public class SalesforceSDKManager implements LifecycleObserver { private boolean useWebServerAuthentication = true; // web server flow ON by default - but app can opt out by calling setUseWebServerAuthentication(false) private boolean useHybridAuthentication = true; // hybrid authentication flows ON by default - but app can opt out by calling setUseHybridAuthentication(false) + + private boolean shouldInferCustomDomain = true; // Detect use of Custom Domain input from login webview - but app can opt out by calling setInferCustomDomain(false) + private Theme theme = Theme.SYSTEM_DEFAULT; private String appName; @@ -672,6 +675,23 @@ public synchronized void setUseHybridAuthentication(boolean useHybridAuthenticat this.useHybridAuthentication = useHybridAuthentication; } + /** + * Returns whether the SDK should infer if the user has entered a new login server through + * the "Use Custom Domain" button on the login screen. + */ + public boolean shouldInferCustomDomain() { + return this.shouldInferCustomDomain; + } + + /** + * Sets whether the SDK should infer if the user has entered a new login server through + * the "Use Custom Domain" button on the login screen. + * @param shouldInferCustomDomain + */ + public synchronized void setShouldInferCustomDomain(boolean shouldInferCustomDomain) { + this.shouldInferCustomDomain = shouldInferCustomDomain; + } + /** * Returns whether the IDP login flow is enabled. * diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index 9d4a02502e..2620fac931 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java @@ -525,7 +525,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request } // Check if user entered a custom domain - if (isNewLoginUrl(uri)) { + if (SalesforceSDKManager.getInstance().shouldInferCustomDomain() && isNewLoginUrl(uri)) { try { String baseUrl = "https://" + uri.getHost(); LoginServerManager serverManager = SalesforceSDKManager.getInstance().getLoginServerManager(); From 70b4a8b55df7dfc0be0663228bbe2dc84bdaf4d2 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Tue, 15 Aug 2023 16:30:45 -0700 Subject: [PATCH 20/24] Improve check for my domain url. --- .../androidsdk/ui/OAuthWebviewHelper.java | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index 2620fac931..187a326b11 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java @@ -550,7 +550,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request String formattedUrl = uri.toString().replace("///", "/").toLowerCase(Locale.US); String callbackUrl = loginOptions.getOauthCallbackUrl().replace("///", "/").toLowerCase(Locale.US); - boolean authFlowFinished = formattedUrl.startsWith(callbackUrl); + boolean authFlowFinished = formattedUrl.startsWith(callbackUrl); if (authFlowFinished) { Map params = UriFragmentParser.parse(uri); String error = params.get("error"); @@ -569,37 +569,22 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request onAuthFlowComplete(tr); } } - } else { - // Only allow redirect for requests with authorization headers for Salesforce urls. - if (request.getRequestHeaders() != null && request.getRequestHeaders().containsKey("Authorization")) { - return isSalesforceUrl(formattedUrl); - } } return authFlowFinished; } - // List from https://help.salesforce.com/s/articleView?language=en_US&id=sf.domain_name_url_formats.htm&type=5 - private boolean isSalesforceUrl(String host) { - return (host != null && (host.endsWith(".salesforce.com") || - host.endsWith(".force.com") || - host.endsWith(".sfdcopens.com") || - host.endsWith(".site.com") || - host.endsWith(".lightning.com") || - host.endsWith(".salesforce-sites.com") || - host.endsWith(".force-user-content.com") || - host.endsWith(".salesforce-experience.com") || - host.endsWith(".salesforce-scrt.com"))); - } - private boolean isNewLoginUrl(Uri uri) { - if (!uri.toString().contains(getLoginUrl()) && isSalesforceUrl(uri.getHost())) { - String path = uri.getQuery(); - return path != null && - path.startsWith("startURL=/setup/secur/RemoteAccessAuthorizationPage.apexp?source="); + String host = uri.getHost(); + String path = uri.getQuery(); + + if (host == null || path == null || getLoginUrl().contains(host)) { + return false; } - return false; + final String myDomainHost = ".my.salesforce.com"; + final String loginPath = "startURL=/setup/secur/RemoteAccessAuthorizationPage.apexp"; + return host.endsWith(myDomainHost) && path.startsWith(loginPath); } @Override From 846a36234bcd700eb2422133ce8b6e230d367a4f Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Tue, 15 Aug 2023 17:01:56 -0700 Subject: [PATCH 21/24] Change infer custom domain to opt-in. --- .../src/com/salesforce/androidsdk/app/SalesforceSDKManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java index 3c04cba788..d71b18b3e2 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java @@ -182,7 +182,7 @@ public class SalesforceSDKManager implements LifecycleObserver { private boolean useHybridAuthentication = true; // hybrid authentication flows ON by default - but app can opt out by calling setUseHybridAuthentication(false) - private boolean shouldInferCustomDomain = true; // Detect use of Custom Domain input from login webview - but app can opt out by calling setInferCustomDomain(false) + private boolean shouldInferCustomDomain = false; // Do not detect use of Custom Domain input from login webview but app can opt in by calling setInferCustomDomain(ture) private Theme theme = Theme.SYSTEM_DEFAULT; private String appName; From d00bf995f72b531742a563ef55f43895f1b8b06d Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Thu, 17 Aug 2023 14:30:07 -0700 Subject: [PATCH 22/24] Rollback change to use instance url for token refresh. --- .../com/salesforce/androidsdk/rest/ClientManager.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java index 55176f12c2..c9566078da 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java @@ -653,6 +653,8 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { final AccountManager mgr = AccountManager.get(context); final String encryptionKey = SalesforceSDKManager.getEncryptionKey(); final String refreshToken = SalesforceSDKManager.decrypt(mgr.getPassword(account), encryptionKey); + final String loginServer = SalesforceSDKManager.decrypt(mgr.getUserData(account, + AuthenticatorService.KEY_LOGIN_URL), encryptionKey); final String clientId = SalesforceSDKManager.decrypt(mgr.getUserData(account, AuthenticatorService.KEY_CLIENT_ID), encryptionKey); final String instServer = SalesforceSDKManager.decrypt(mgr.getUserData(account, @@ -660,7 +662,6 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { final String communityUrl = SalesforceSDKManager.decrypt(mgr.getUserData(account, AuthenticatorService.KEY_COMMUNITY_URL), encryptionKey); - final String url = (communityUrl != null) ? communityUrl : instServer; final List additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys(); Map values = null; if (additionalOauthKeys != null && !additionalOauthKeys.isEmpty()) { @@ -676,11 +677,15 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { final Map addlParamsMap = SalesforceSDKManager.getInstance().getLoginOptions().getAdditionalParameters(); try { final OAuth2.TokenEndpointResponse tr = OAuth2.refreshAuthToken(HttpAccess.DEFAULT, - new URI(url), clientId, refreshToken, addlParamsMap); + new URI(loginServer), clientId, refreshToken, addlParamsMap); if (!instServer.equalsIgnoreCase(tr.instanceUrl)) { mgr.setUserData(account, AuthenticatorService.KEY_INSTANCE_URL, SalesforceSDKManager.encrypt(tr.instanceUrl, encryptionKey)); } + if (communityUrl != null && !communityUrl.equalsIgnoreCase(tr.communityUrl)) { + mgr.setUserData(account, AuthenticatorService.KEY_COMMUNITY_URL, + SalesforceSDKManager.encrypt(tr.communityUrl, encryptionKey)); + } mgr.setUserData(account, AuthenticatorService.KEY_LIGHTNING_DOMAIN, SalesforceSDKManager.encrypt(tr.lightningDomain, encryptionKey)); mgr.setUserData(account, AuthenticatorService.KEY_LIGHTNING_SID, From 1c75ec4be4589a83cfa0b1b54b7973b3a7f3ea20 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Thu, 17 Aug 2023 17:47:35 -0700 Subject: [PATCH 23/24] Update isNewLoginUrl check. --- .../com/salesforce/androidsdk/ui/OAuthWebviewHelper.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index 187a326b11..dd55e7faf3 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java @@ -576,15 +576,16 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request private boolean isNewLoginUrl(Uri uri) { String host = uri.getHost(); - String path = uri.getQuery(); + String query = uri.getQuery(); + String path = uri.getPath(); - if (host == null || path == null || getLoginUrl().contains(host)) { + if (host == null || query == null || path == null || getLoginUrl().contains(host)) { return false; } final String myDomainHost = ".my.salesforce.com"; final String loginPath = "startURL=/setup/secur/RemoteAccessAuthorizationPage.apexp"; - return host.endsWith(myDomainHost) && path.startsWith(loginPath); + return host.endsWith(myDomainHost) && query.startsWith(loginPath) && path.equals("/"); } @Override From 8be650ddf72c87069010ad7d802597310e286f44 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Thu, 24 Aug 2023 18:52:59 -0700 Subject: [PATCH 24/24] Update Custom Domain opt-in boolean to be opt-in regex pattern. --- .../androidsdk/app/SalesforceSDKManager.java | 27 ++++++++++++------- .../androidsdk/ui/OAuthWebviewHelper.java | 20 ++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java index d71b18b3e2..9f0fb10ea3 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java @@ -48,6 +48,7 @@ import android.webkit.CookieManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; @@ -100,6 +101,7 @@ import java.util.SortedSet; import java.util.UUID; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.regex.Pattern; /** * This class serves as an interface to the various @@ -182,7 +184,7 @@ public class SalesforceSDKManager implements LifecycleObserver { private boolean useHybridAuthentication = true; // hybrid authentication flows ON by default - but app can opt out by calling setUseHybridAuthentication(false) - private boolean shouldInferCustomDomain = false; // Do not detect use of Custom Domain input from login webview but app can opt in by calling setInferCustomDomain(ture) + private Pattern customDomainInferencePattern; private Theme theme = Theme.SYSTEM_DEFAULT; private String appName; @@ -676,20 +678,25 @@ public synchronized void setUseHybridAuthentication(boolean useHybridAuthenticat } /** - * Returns whether the SDK should infer if the user has entered a new login server through - * the "Use Custom Domain" button on the login screen. + * Returns the pattern used to detect the use of "Use Custom Domain" input from login web view. + * + * @return pattern if set or null */ - public boolean shouldInferCustomDomain() { - return this.shouldInferCustomDomain; + public synchronized Pattern getCustomDomainInferencePattern() { + return customDomainInferencePattern; } /** - * Sets whether the SDK should infer if the user has entered a new login server through - * the "Use Custom Domain" button on the login screen. - * @param shouldInferCustomDomain + * Detect use of "Use Custom Domain" input from login web view using the given regex. + * Example for a specific org: + * "^https:\\/\\/mobilesdk\\.my\\.salesforce\\.com\\/\\?startURL=%2Fsetup%2Fsecur%2FRemoteAccessAuthorizationPage\\.apexp" + * For any my domain: + * "^https:\\/\\/[a-zA-Z0-9]+\\.my\\.salesforce\\.com/\\?startURL=%2Fsetup%2Fsecur%2FRemoteAccessAuthorizationPage\\.apexp" + * + * @param pattern regex to use when detecting use of custom domain on login */ - public synchronized void setShouldInferCustomDomain(boolean shouldInferCustomDomain) { - this.shouldInferCustomDomain = shouldInferCustomDomain; + public synchronized void setCustomDomainInferencePattern(@Nullable Pattern pattern) { + this.customDomainInferencePattern = pattern; } /** diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index dd55e7faf3..47a6f7d317 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java @@ -100,6 +100,7 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.regex.Pattern; import okhttp3.Request; import okhttp3.Response; @@ -525,7 +526,10 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request } // Check if user entered a custom domain - if (SalesforceSDKManager.getInstance().shouldInferCustomDomain() && isNewLoginUrl(uri)) { + String host = uri.getHost(); + Pattern customDomainPattern = SalesforceSDKManager.getInstance().getCustomDomainInferencePattern(); + if (host != null && !getLoginUrl().contains(host) && customDomainPattern != null + && customDomainPattern.matcher(uri.toString()).find()) { try { String baseUrl = "https://" + uri.getHost(); LoginServerManager serverManager = SalesforceSDKManager.getInstance().getLoginServerManager(); @@ -574,20 +578,6 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request return authFlowFinished; } - private boolean isNewLoginUrl(Uri uri) { - String host = uri.getHost(); - String query = uri.getQuery(); - String path = uri.getPath(); - - if (host == null || query == null || path == null || getLoginUrl().contains(host)) { - return false; - } - - final String myDomainHost = ".my.salesforce.com"; - final String loginPath = "startURL=/setup/secur/RemoteAccessAuthorizationPage.apexp"; - return host.endsWith(myDomainHost) && query.startsWith(loginPath) && path.equals("/"); - } - @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { int primError = error.getPrimaryError();