@@ -32,7 +32,6 @@ import android.accounts.AccountAuthenticatorResponse
3232import android.accounts.AccountManager.ERROR_CODE_CANCELED
3333import android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE
3434import android.annotation.SuppressLint
35- import android.app.Activity
3635import android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD
3736import android.content.Context
3837import android.content.Intent
@@ -74,8 +73,9 @@ import androidx.activity.addCallback
7473import androidx.activity.compose.setContent
7574import androidx.activity.enableEdgeToEdge
7675import androidx.activity.result.ActivityResult
76+ import androidx.activity.result.ActivityResultCallback
7777import androidx.activity.result.ActivityResultLauncher
78- import androidx.activity.result.contract.ActivityResultContracts
78+ import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
7979import androidx.activity.viewModels
8080import androidx.annotation.VisibleForTesting
8181import androidx.annotation.VisibleForTesting.Companion.PROTECTED
@@ -105,7 +105,7 @@ import androidx.core.content.ContextCompat.getMainExecutor
105105import androidx.core.net.toUri
106106import androidx.core.view.WindowCompat
107107import androidx.fragment.app.FragmentActivity
108- import androidx.lifecycle.lifecycleScope
108+ import androidx.lifecycle.Observer
109109import com.salesforce.androidsdk.R.color.sf__background
110110import com.salesforce.androidsdk.R.color.sf__background_dark
111111import com.salesforce.androidsdk.R.color.sf__primary_color
@@ -147,7 +147,6 @@ import com.salesforce.androidsdk.util.SalesforceSDKLogger.e
147147import com.salesforce.androidsdk.util.SalesforceSDKLogger.w
148148import com.salesforce.androidsdk.util.UriFragmentParser
149149import kotlinx.coroutines.CoroutineScope
150- import kotlinx.coroutines.Dispatchers
151150import kotlinx.coroutines.Dispatchers.Default
152151import kotlinx.coroutines.Dispatchers.IO
153152import kotlinx.coroutines.launch
@@ -175,6 +174,10 @@ import java.security.cert.X509Certificate
175174 * them.
176175 */
177176open class LoginActivity : FragmentActivity () {
177+
178+ /* * The activity result launcher used when browser-based authentication loads the OAuth authorization URL in the external browser custom tab activity */
179+ private val customTabLauncher = registerForActivityResult(StartActivityForResult (), CustomTabActivityResult ())
180+
178181 // View Model
179182 @VisibleForTesting(otherwise = PROTECTED )
180183 open val viewModel: LoginViewModel
@@ -183,6 +186,7 @@ open class LoginActivity : FragmentActivity() {
183186 // Webview and Clients
184187 @VisibleForTesting(otherwise = PROTECTED )
185188 open val webViewClient = AuthWebViewClient ()
189+
186190 @VisibleForTesting(otherwise = PROTECTED )
187191 open val webChromeClient = WebChromeClient ()
188192 open val webView: WebView by lazy {
@@ -226,7 +230,7 @@ open class LoginActivity : FragmentActivity() {
226230 SalesforceSDKManager .getInstance().setViewNavigationVisibility(this )
227231 }
228232
229- applyIntent(intent )
233+ applyIntent()
230234
231235 // Don't let sharedBrowserSession org setting stop a new user from logging in.
232236 if (intent.extras?.getBoolean(NEW_USER ) == true ) {
@@ -272,62 +276,9 @@ open class LoginActivity : FragmentActivity() {
272276 onBackPressedDispatcher.addCallback { handleBackBehavior() }
273277 }
274278
275- val customTabLauncher: ActivityResultLauncher <Intent > = registerForActivityResult(
276- ActivityResultContracts .StartActivityForResult ()
277- ) { result: ActivityResult ->
278- // Check if the user backed out of the custom tab.
279- if (result.resultCode == Activity .RESULT_CANCELED ) {
280- if (viewModel.singleServerCustomTabActivity) {
281- // Show blank page and spinner until PKCE is done.
282- viewModel.loginUrl.value = ABOUT_BLANK
283- } else {
284- // Don't show server picker if we are re-authenticating with cookie.
285- clearWebView(showServerPicker = ! sharedBrowserSession)
286- }
287- }
288- }
289-
290- // Take action on selected server change.
291- viewModel.selectedServer.observe(this ) { selectedServer ->
292-
293- // Guard against observing a selected server already provided by the intent data, such as a Salesforce Welcome Discovery mobile URL.
294- val selectedServerUri = selectedServer.toUri()
295- if (intent.data?.host == selectedServerUri.host) {
296- return @observe
297- }
298-
299- // Use the URL to switch between default or Salesforce Welcome Discovery log in, if applicable.
300- if (switchDefaultOrSalesforceWelcomeDiscoveryLogin(selectedServerUri)) {
301- return @observe
302- }
303-
304- if (viewModel.singleServerCustomTabActivity) {
305- // Skip fetching authorization and show custom tab immediately.
306- viewModel.reloadWebView()
307- viewModel.loginUrl.value?.let { url ->
308- loadLoginPageInCustomTab(url, customTabLauncher)
309- }
310- } else {
311- with (SalesforceSDKManager .getInstance()) {
312- // Fetch well known config and load in custom tab if required.
313- fetchAuthenticationConfiguration {
314- /* Browser-based authentication is applicable when not authenticating with a front-door bridge URL */
315- if (isBrowserLoginEnabled && ! viewModel.isUsingFrontDoorBridge) {
316- if (useWebServerAuthentication) {
317- viewModel.loginUrl.value?.let { url -> loadLoginPageInCustomTab(url, customTabLauncher) }
318- } else {
319- /* Reload the webview now that isBrowserLoginEnabled has been set
320- to true so that we generate an authorization URL with PKCE values. */
321- lifecycleScope.launch(Dispatchers .Main ) {
322- viewModel.reloadWebView()
323- viewModel.loginUrl.value?.let { url -> loadLoginPageInCustomTab(url, customTabLauncher) }
324- }
325- }
326- }
327- }
328- }
329- }
330- }
279+ // Add view model observers.
280+ viewModel.browserCustomTabUrl.observe(this , BrowserCustomTabUrlObserver ())
281+ viewModel.pendingServer.observe(this , PendingServerObserver ())
331282
332283 // Support magic links
333284 if (viewModel.jwt != null ) {
@@ -359,7 +310,10 @@ open class LoginActivity : FragmentActivity() {
359310 return
360311 }
361312
362- applyIntent(intent)
313+ // Store the new intent and apply it to the activity.
314+ setIntent(intent)
315+ applyIntent()
316+ viewModel.applyPendingServer(pendingLoginServer = viewModel.pendingServer.value)
363317 }
364318
365319 private fun clearWebView (showServerPicker : Boolean = true) {
@@ -984,52 +938,38 @@ open class LoginActivity : FragmentActivity() {
984938 )
985939 .build()
986940
987- /* *
988- * Determines if the provided proposed selected server URL is a switch from
989- * Salesforce Welcome Discovery back to default log in.
990- * @param proposedSelectedServerUrl The proposed selected server URL
991- * @return Boolean true if the provided proposed selected server URL is a
992- * switch from Salesforce Welcome Discovery back to the default log in,
993- * false otherwise.
994- * */
995- private fun isSwitchFromSalesforceWelcomeDiscoveryToDefaultLogin (
996- proposedSelectedServerUrl : Uri
997- ) = viewModel.loginUrl.value?.toUri()?.let { loginUrl ->
998- isSalesforceWelcomeDiscoveryMobileUrl(this , loginUrl) && ! (isSalesforceWelcomeDiscoveryMobileUrl(this , proposedSelectedServerUrl))
999- } ? : false
1000-
1001941 /* *
1002942 * Switches between default or Salesforce Welcome Discovery log in as needed
1003- * using the provided proposed selected server URL.
1004- * @param uri The proposed selected server URL
943+ * using the provided pending login server URL.
944+ * @param pendingLoginServerUri The pending login server URL
1005945 * @return Boolean true if a switch between default or Salesforce Welcome
1006946 * Discovery log is made, false otherwise.
1007947 */
1008- private fun switchDefaultOrSalesforceWelcomeDiscoveryLogin (uri : Uri ) =
948+ private fun switchDefaultOrSalesforceWelcomeDiscoveryLogin (pendingLoginServerUri : Uri ) =
1009949
1010- // If the selected server has changed to a new Salesforce Welcome Discovery URL and host.
1011- if (isSalesforceWelcomeDiscoveryUrlPath(uri )) {
950+ // If the pending login server is a change to a new Salesforce Welcome Discovery URL and host.
951+ if (isSalesforceWelcomeDiscoveryUrlPath(pendingLoginServerUri )) {
1012952
1013953 // Navigate to Salesforce Welcome Discovery.
1014954 startActivity(
1015955 Intent (
1016956 this ,
1017- LoginActivity :: class .java
957+ SalesforceSDKManager .getInstance().webViewLoginActivityClass
1018958 ).apply {
1019- data = generateSalesforceWelcomeDiscoveryMobileUrl(uri )
959+ data = generateSalesforceWelcomeDiscoveryMobileUrl(pendingLoginServerUri )
1020960 flags = FLAG_ACTIVITY_SINGLE_TOP
1021961 })
1022962 true
1023963 }
1024964
1025- // If the new selected server isn't a Salesforce Welcome Discovery URL but the previous was...
1026- else if (isSwitchFromSalesforceWelcomeDiscoveryToDefaultLogin(uri )) {
965+ // If the pending login server isn't a Salesforce Welcome Discovery URL but the previous was...
966+ else if (viewModel. isSwitchFromSalesforceWelcomeDiscoveryToDefaultLogin(pendingLoginServerUri )) {
1027967
1028- // Navigate to login.
968+ // Navigate to default login.
1029969 startActivity(
1030970 Intent (
1031971 this ,
1032- LoginActivity :: class .java
972+ SalesforceSDKManager .getInstance().webViewLoginActivityClass
1033973 ).apply {
1034974 flags = FLAG_ACTIVITY_SINGLE_TOP
1035975 })
@@ -1075,11 +1015,10 @@ open class LoginActivity : FragmentActivity() {
10751015 // endregion
10761016
10771017 /* *
1078- * Applies a new intent to the activity, for instance when the activity is
1079- * created or receives a new intent.
1080- * @param intent The new intent
1018+ * (Re-)applies the intent to the activity, for instance when the activity
1019+ * is created or receives a new intent.
10811020 */
1082- private fun applyIntent (intent : Intent ) {
1021+ private fun applyIntent () {
10831022
10841023 // If the intent is for Salesforce Welcome Discovery, apply it to the activity.
10851024 applySalesforceWelcomeDiscoveryIntent(intent)
@@ -1088,6 +1027,34 @@ open class LoginActivity : FragmentActivity() {
10881027 applyUiBridgeApiFrontDoorUrl(intent)
10891028 }
10901029
1030+ /* *
1031+ * Starts a browser custom tab for the OAuth authorization URL according to
1032+ * the authentication configuration. The activity only takes action when
1033+ * browser-based authentication requires a browser custom tab to be started.
1034+ * UI front-door bridge use bypasses the need for browser custom tab.
1035+ * @param authorizationUrl The selected login server's OAuth authorization
1036+ * URL
1037+ * @param activityResultLauncher The activity result launcher to use when
1038+ * browser-based authentication requires a browser custom tab
1039+ * @param isBrowserLoginEnabled Indicates if browser-based authentication is
1040+ * enabled
1041+ * @param isUsingFrontDoorBridge Indicates if a UI bridge API front door
1042+ * bridge URL is in use
1043+ * @param singleServerCustomTabActivity Indicates single server custom
1044+ * browser tab authentication is active
1045+ */
1046+ private fun startBrowserCustomTabAuthorization (
1047+ authorizationUrl : String ,
1048+ activityResultLauncher : ActivityResultLauncher <Intent >,
1049+ isBrowserLoginEnabled : Boolean = SalesforceSDKManager .getInstance().isBrowserLoginEnabled,
1050+ isUsingFrontDoorBridge : Boolean = viewModel.isUsingFrontDoorBridge,
1051+ singleServerCustomTabActivity : Boolean = viewModel.singleServerCustomTabActivity,
1052+ ) {
1053+ if ((singleServerCustomTabActivity.or (isBrowserLoginEnabled)).and (! isUsingFrontDoorBridge)) {
1054+ loadLoginPageInCustomTab(authorizationUrl, activityResultLauncher)
1055+ }
1056+ }
1057+
10911058 /* *
10921059 * A web view client which intercepts the redirect to the OAuth callback URL. That redirect marks the end of
10931060 * the user facing portion of the authentication flow.
@@ -1478,13 +1445,15 @@ open class LoginActivity : FragmentActivity() {
14781445 ): Boolean {
14791446 if (! uri.isHierarchical) return false
14801447 val clientIdParameter = uri.getQueryParameter(SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CLIENT_ID )
1481- return isSalesforceWelcomeDiscoveryUrlPath(uri) && uri.queryParameterNames?.contains(
1448+ val isDiscovery = isSalesforceWelcomeDiscoveryUrlPath(uri)
1449+
1450+ return isDiscovery && uri.queryParameterNames.contains(
14821451 SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CLIENT_ID
1483- ) != null && uri.queryParameterNames? .contains(
1452+ ) && uri.queryParameterNames.contains(
14841453 SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CLIENT_VERSION
1485- ) != null && uri.queryParameterNames? .contains(
1454+ ) && uri.queryParameterNames.contains(
14861455 SALESFORCE_WELCOME_DISCOVERY_MOBILE_URL_QUERY_PARAMETER_KEY_CALLBACK_URL
1487- ) != null && clientIdParameter == getBootConfig(context).remoteAccessConsumerKey &&
1456+ ) && clientIdParameter == getBootConfig(context).remoteAccessConsumerKey &&
14881457 (clientIdParameter == " SfdcMobileChatterAndroid" || clientIdParameter == " SfdcMobileChatteriOS" ) // TODO: Keep this list of client ids up to date with those supported by Salesforce Welcome Discovery or remove it when no longer required.
14891458 }
14901459
@@ -1511,7 +1480,7 @@ open class LoginActivity : FragmentActivity() {
15111480 loginHint : String ,
15121481 loginHost : String ,
15131482 ) {
1514- Intent (context, LoginActivity :: class .java ).apply {
1483+ Intent (context, SalesforceSDKManager .getInstance().webViewLoginActivityClass ).apply {
15151484 putExtra(EXTRA_KEY_LOGIN_HINT , loginHint)
15161485 putExtra(EXTRA_KEY_LOGIN_HOST , loginHost)
15171486 flags = FLAG_ACTIVITY_SINGLE_TOP
@@ -1521,4 +1490,87 @@ open class LoginActivity : FragmentActivity() {
15211490
15221491 // endregion
15231492 }
1493+
1494+ // region Activity Result Callback Classes
1495+
1496+ /* *
1497+ * An activity result callback used when browser-based authentication loads
1498+ * the OAuth authorization URL in the external browser custom tab activity.
1499+ * @param activity The login activity. This parameter is intended for
1500+ * testing purposes only. Defaults to this inner class receiver
1501+ */
1502+ private inner class CustomTabActivityResult (
1503+ private val activity : LoginActivity = this@LoginActivity
1504+ ) : ActivityResultCallback<ActivityResult> {
1505+
1506+ override fun onActivityResult (result : ActivityResult ) {
1507+ // Check if the user backed out of the custom tab.
1508+ if (result.resultCode == RESULT_CANCELED ) {
1509+ if (activity.viewModel.singleServerCustomTabActivity) {
1510+ // Show blank page and spinner until PKCE is done.
1511+ activity.viewModel.loginUrl.value = ABOUT_BLANK
1512+ } else {
1513+ // Don't show server picker if we are re-authenticating with cookie.
1514+ activity.clearWebView(showServerPicker = ! activity.sharedBrowserSession)
1515+ }
1516+ }
1517+ }
1518+ }
1519+
1520+ // endregion
1521+ // region Observer Classes
1522+
1523+ /* *
1524+ * An observer for browser custom tab URL that continues the authentication
1525+ * flow by loading the login URL in a web browser custom tab when browser-
1526+ * based authentication is required.
1527+ * @param activity The login activity. This parameter is intended for
1528+ * testing purposes only. Defaults to this inner class receiver
1529+ */
1530+ internal inner class BrowserCustomTabUrlObserver (
1531+ private val activity : LoginActivity = this@LoginActivity
1532+ ) : Observer<String> {
1533+ override fun onChanged (value : String ) {
1534+ if (value == " about:blank" ) {
1535+ return
1536+ }
1537+
1538+ activity.startBrowserCustomTabAuthorization(
1539+ authorizationUrl = value,
1540+ activityResultLauncher = activity.customTabLauncher,
1541+ isBrowserLoginEnabled = SalesforceSDKManager .getInstance().isBrowserLoginEnabled,
1542+ )
1543+ }
1544+ }
1545+
1546+ /* *
1547+ * An observer for pending login server that continues the authentication
1548+ * flow by determining the switch between default login and Salesforce
1549+ * Welcome Discovery before applying the pending login server to the
1550+ * activity.
1551+ * @param activity The login activity. This parameter is intended for
1552+ * testing purposes only. Defaults to this inner class receiver
1553+ */
1554+ private inner class PendingServerObserver (
1555+ private val activity : LoginActivity = this@LoginActivity
1556+ ) : Observer<String> {
1557+ override fun onChanged (value : String ) {
1558+ // Guard against observing a pending login server already provided by the intent data, such as a Salesforce Welcome Discovery mobile URL.
1559+ val pendingServerUri = value.toUri()
1560+ if (activity.intent.data?.host == pendingServerUri.host || activity.intent.getStringExtra(EXTRA_KEY_LOGIN_HOST ) == pendingServerUri.host) {
1561+ activity.viewModel.previousPendingServer = value
1562+ return
1563+ }
1564+
1565+ // Use the URL to switch between default or Salesforce Welcome Discovery log in, if applicable.
1566+ if (activity.switchDefaultOrSalesforceWelcomeDiscoveryLogin(pendingServerUri)) {
1567+ activity.viewModel.previousPendingServer = value
1568+ return
1569+ }
1570+
1571+ activity.viewModel.applyPendingServer(pendingLoginServer = value)
1572+ }
1573+ }
1574+
1575+ // endregion
15241576}
0 commit comments