diff --git a/.circleci/config.yml b/.circleci/config.yml index 576c38b2da..4f5fca2935 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@12.0.0 sudo npm install -g typescript cordova telemetry off ./install.sh @@ -374,9 +374,11 @@ 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 matrix: parameters: lib: ["SalesforceAnalytics", "SalesforceSDK", "SmartStore", "MobileSync", "SalesforceHybrid", "SalesforceReact"] @@ -401,12 +403,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 >>] 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/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/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/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/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/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/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/res/values/sf__strings.xml b/libs/SalesforceSDK/res/values/sf__strings.xml index d0af172215..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 @@ -50,11 +50,12 @@ 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 Failed to exchange authorization code - Login complete + Log in complete Login request sent to SP app @@ -62,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. @@ -90,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/app/SalesforceSDKManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.java index 9f5d25657e..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 @@ -114,7 +116,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 @@ -181,6 +183,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 Pattern customDomainInferencePattern; + private Theme theme = Theme.SYSTEM_DEFAULT; private String appName; @@ -672,6 +677,28 @@ public synchronized void setUseHybridAuthentication(boolean useHybridAuthenticat this.useHybridAuthentication = useHybridAuthentication; } + /** + * Returns the pattern used to detect the use of "Use Custom Domain" input from login web view. + * + * @return pattern if set or null + */ + public synchronized Pattern getCustomDomainInferencePattern() { + return customDomainInferencePattern; + } + + /** + * 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 setCustomDomainInferencePattern(@Nullable Pattern pattern) { + this.customDomainInferencePattern = pattern; + } + /** * Returns whether the IDP login flow is enabled. * 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/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/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java index 4d10f83e7e..c9566078da 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/rest/ClientManager.java @@ -659,6 +659,9 @@ 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 communityUrl = SalesforceSDKManager.decrypt(mgr.getUserData(account, + AuthenticatorService.KEY_COMMUNITY_URL), encryptionKey); + final List additionalOauthKeys = SalesforceSDKManager.getInstance().getAdditionalOauthKeys(); Map values = null; if (additionalOauthKeys != null && !additionalOauthKeys.isEmpty()) { @@ -679,6 +682,10 @@ private Bundle refreshStaleToken(Account account) throws NetworkErrorException { 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, 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); 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/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/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) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/OAuthWebviewHelper.java index 4893e18ffc..47a6f7d317 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; @@ -99,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; @@ -473,7 +475,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 +502,7 @@ public void onPageFinished(WebView view, String url) { } } } + EventsObservable.get().notifyEvent(EventType.AuthWebViewPageFinished, url); super.onPageFinished(view, url); } @@ -522,8 +525,37 @@ 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) { + // Check if user entered a custom domain + 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(); + 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); + if (authFlowFinished) { Map params = UriFragmentParser.parse(uri); String error = params.get("error"); // Did we fail? @@ -542,7 +574,8 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request } } } - return isDone; + + return authFlowFinished; } @Override @@ -570,6 +603,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(); + } + } } /** 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/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java b/libs/SmartStore/src/com/salesforce/androidsdk/smartstore/store/KeyValueEncryptedFileStore.java index 722462e723..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; @@ -463,13 +462,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/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"; 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/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 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 // 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..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,6 +53,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Random; @RunWith(AndroidJUnit4.class) public class KeyValueEncryptedFileStoreTest { @@ -251,6 +252,8 @@ public void testSaveValueGetValue() { } } + + /** Test saving from streams and getting them back as values */ @Test public void testSaveStreamGetValue() { @@ -267,6 +270,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() { 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}}"); 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/publish/publish-module.gradle b/publish/publish-module.gradle index b98bdd55cf..838e76a92f 100644 --- a/publish/publish-module.gradle +++ b/publish/publish-module.gradle @@ -9,9 +9,8 @@ android { } } -task sourcesJar(type: Jar) { +tasks.register('sourcesJar', Jar) { from android.sourceSets.main.java.srcDirs - archiveClassifier.set('sources') } artifacts { @@ -52,9 +51,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 = 'JohnsonEricAtSalesforce' + name = 'Eric Johnson' + email = 'johnson.eric@salesforce.com' } } scm { 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