Skip to content

@W-19368426: [MSDK] Rewrite Android Screen Lock (a.k.a Passcode) for RTL UI#2835

Open
JohnsonEricAtSalesforce wants to merge 7 commits intoforcedotcom:devfrom
JohnsonEricAtSalesforce:feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui
Open

@W-19368426: [MSDK] Rewrite Android Screen Lock (a.k.a Passcode) for RTL UI#2835
JohnsonEricAtSalesforce wants to merge 7 commits intoforcedotcom:devfrom
JohnsonEricAtSalesforce:feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui

Conversation

@JohnsonEricAtSalesforce
Copy link
Contributor

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce commented Feb 4, 2026

🎸 Ready For Review 🥁

This updates the screen lock setup activity to support right-to-left text layouts along with a much heftier effort to migrate from Java to Kotlin, layout using Jetpack Compose and provide comprehensive unit tests.

All deprecations are resolved as well and the sources should have no inspector warnings.

To test, provide the strings used by the activity in an RTL language such as Arabic. Then, set the system language to that language.

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

1 Warning
⚠️ Big PR, try to keep changes smaller if you can.

Generated by 🚫 Danger

@@ -22,6 +22,7 @@ dependencies {
api("androidx.browser:browser:1.8.0") // Update requires API 36 compileSdk
Copy link

@github-actions github-actions bot Feb 5, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of androidx.browser:browser than 1.8.0 is available: 1.9.0

@@ -22,6 +22,7 @@ dependencies {
api("androidx.browser:browser:1.8.0") // Update requires API 36 compileSdk
api("androidx.work:work-runtime-ktx:2.10.3")
Copy link

@github-actions github-actions bot Feb 5, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of androidx.work:work-runtime-ktx than 2.10.3 is available: 2.11.1

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

23 Warnings
⚠️ libs/SalesforceSDK/build.gradle.kts#L19 - A newer version of com.squareup.okhttp3:okhttp than 4.12.0 is available: 5.3.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L20 - A newer version of com.google.firebase:firebase-messaging than 25.0.0 is available: 25.0.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L21 - A newer version of androidx.core:core than 1.16.0 is available: 1.17.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L30 - A newer version of androidx.core:core-ktx than 1.16.0 is available: 1.17.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L31 - A newer version of androidx.activity:activity-ktx than 1.10.1 is available: 1.12.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L32 - A newer version of androidx.activity:activity-compose than 1.10.1 is available: 1.12.3
⚠️ libs/SalesforceSDK/build.gradle.kts#L33 - A newer version of androidx.lifecycle:lifecycle-viewmodel-ktx than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L34 - A newer version of androidx.lifecycle:lifecycle-viewmodel-compose than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L35 - A newer version of androidx.lifecycle:lifecycle-viewmodel-savedstate than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L36 - A newer version of androidx.lifecycle:lifecycle-service than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L37 - A newer version of org.jetbrains.kotlinx:kotlinx-serialization-json than 1.6.3 is available: 1.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L38 - A newer version of androidx.window:window than 1.4.0 is available: 1.5.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L39 - A newer version of androidx.window:window-core than 1.4.0 is available: 1.5.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L40 - A newer version of androidx.compose.material3:material3-android than 1.3.2 is available: 1.4.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L41 - A newer version of androidx.compose:compose-bom than 2025.07.00 is available: 2026.01.01
⚠️ libs/SalesforceSDK/build.gradle.kts#L42 - A newer version of androidx.compose.foundation:foundation-android than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L43 - A newer version of androidx.compose.runtime:runtime-livedata than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L44 - A newer version of androidx.compose.ui:ui-tooling-preview-android than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L45 - A newer version of androidx.compose.material:material than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L47 - A newer version of androidx.compose.ui:ui-tooling than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L48 - A newer version of androidx.compose.ui:ui-test-manifest than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L54 - A newer version of androidx.compose.ui:ui-test-junit4 than 1.8.2 is available: 1.10.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L55 - A newer version of io.mockk:mockk-android than 1.14.0 is available: 1.14.9

Generated by 🚫 Danger

@codecov
Copy link

codecov bot commented Feb 5, 2026

Codecov Report

❌ Patch coverage is 96.60194% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.89%. Comparing base (c9e962d) to head (33fc0b1).
⚠️ Report is 6 commits behind head on dev.

Files with missing lines Patch % Lines
...esforce/androidsdk/ui/components/ScreenLockView.kt 91.48% 0 Missing and 4 partials ⚠️
...com/salesforce/androidsdk/ui/ScreenLockActivity.kt 97.88% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                dev    #2835      +/-   ##
============================================
+ Coverage     63.24%   63.89%   +0.65%     
- Complexity     2825     2874      +49     
============================================
  Files           216      218       +2     
  Lines         16982    17064      +82     
  Branches       2418     2423       +5     
============================================
+ Hits          10740    10903     +163     
+ Misses         5073     4990      -83     
- Partials       1169     1171       +2     
Components Coverage Δ
Analytics 47.92% <ø> (ø)
SalesforceSDK 57.63% <96.60%> (+1.29%) ⬆️
Hybrid 59.05% <ø> (ø)
SmartStore 78.20% <ø> (ø)
MobileSync 81.68% <ø> (ø)
React 52.36% <ø> (ø)
Files with missing lines Coverage Δ
...rc/com/salesforce/androidsdk/ui/DevInfoActivity.kt 59.43% <ø> (ø)
...m/salesforce/androidsdk/ui/LoginOptionsActivity.kt 87.62% <ø> (ø)
...om/salesforce/androidsdk/ui/ScreenLockViewModel.kt 100.00% <100.00%> (ø)
...ce/androidsdk/ui/components/LoginServerListItem.kt 61.48% <ø> (ø)
...m/salesforce/androidsdk/ui/components/LoginView.kt 43.83% <ø> (ø)
...orce/androidsdk/ui/components/PickerBottomSheet.kt 76.04% <ø> (ø)
...ce/androidsdk/ui/components/UserAccountListItem.kt 54.28% <ø> (ø)
...com/salesforce/androidsdk/ui/ScreenLockActivity.kt 97.88% <97.88%> (ø)
...esforce/androidsdk/ui/components/ScreenLockView.kt 91.48% <91.48%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui branch 2 times, most recently from bcf7c35 to 282337b Compare February 5, 2026 21:23
Copy link
Contributor

@brandonpage brandonpage left a comment

Choose a reason for hiding this comment

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

Just a couple high level comments while you are in draft. 👍

import com.salesforce.androidsdk.R.string.sf__server_remove_content_description
import com.salesforce.androidsdk.R.string.sf__server_url_delete
import com.salesforce.androidsdk.config.LoginServerManager.LoginServer
import com.salesforce.androidsdk.ui.PADDING_SIZE
Copy link
Contributor

@brandonpage brandonpage Feb 5, 2026

Choose a reason for hiding this comment

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

The local PADDING_SIZE is not removed.

Copy link
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce Feb 7, 2026

Choose a reason for hiding this comment

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

When I was basing the new ScreenLockActivity off other recent Compose implementations, I noticed this PADDING_SIZE and CORNER_RADIUS where used in a lot of disparate sources though still defined in LoginView.kt. Doesn't it seem they should be in a common location? I created UserInterfaceConstants.kt but we could do something different. There may be other values as well.

* An activity that locks the app behind the operating system's provided
* authentication.
*/
internal class ScreenLockActivity : FragmentActivity() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically for Semver this should be open class ScreenLockActivity (public). It wasn't possible for us to use a subclass so it shouldn't matter. I don't think this was a bad decision, just throwing it out there for the sake of completeness and conversation.

Copy link
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce Feb 6, 2026

Choose a reason for hiding this comment

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

I went ahead and made it public again as I'm getting ready to mark this ready for review soon ✅

Comment on lines 50 to 54
internal class ScreenLockViewModel(
build: Int = SDK_INT,
// TODO: Test coverage needed. ECJ20260205
val appName: String = getInstance().appName ?: "App",
) : ViewModel() {
Copy link
Contributor

Choose a reason for hiding this comment

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

A ViewModel seems like overkill IMO. Is there a reason it is needed?

Throwing that out for consideration before you spend a ton of time writing tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was wondering as well since at first the view model was relatively light and much of the logic related to screen lock is at the activity/view level. That said, there are some observables and they will benefit from the life cycle management the view model offers. It will also be ready should this grow in complexity. So, I'm leaning towards keeping the view model unless we feel a compelling reason not to.

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui branch 2 times, most recently from a69d97c to a29609b Compare February 5, 2026 22:24
@github-actions
Copy link

github-actions bot commented Feb 5, 2026

Job Summary for Gradle

Pull Request :: test-android
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
SalesforceMobileSDK-Android libs:SalesforceSDK:convertCodeCoverage 8.14.3 Build Scan not published
SalesforceMobileSDK-Android libs:SalesforceSDK:lint 8.14.3 Build Scan not published
SalesforceMobileSDK-Android libs:SalesforceSDK:assembleAndroidTest 8.14.3 Build Scan not published

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui branch 15 times, most recently from 692a6f3 to 954c6b2 Compare February 6, 2026 21:07
@JohnsonEricAtSalesforce
Copy link
Contributor Author

Code coverage is 96.60%. There are a handful of Jetpack Compose function call sites that are beyond the ability of JaCoCo to probe and that's expected from the research I've done. They are tested by the new tests though and I used tooling to validate that.

The tests I created are comprehensive and should provide reasonable assertions for the behavior migrated from Java.

There are a few places where I introduced new ways of tidying the code so it provides better coverage numbers and generates less noise in the code coverage reports.

@JohnsonEricAtSalesforce
Copy link
Contributor Author

Here're screenshots of the English LTR and Arabic RTL I tested with.
20260206 Screen Lock In Arabic RTL
20260206 Screen Lock In English LTR

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce marked this pull request as ready for review February 6, 2026 23:56
@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did a side-by-side comparison of this XML with the new Compose logic plus compared screenshots of the two versions for parity.

<?xml version="1.0" encoding="utf-8"?>

<resources>
<!-- Dimensions used in sf__screen_lock.xml -->
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe these were the only orphaned dimensions after removing the XML layout.

<item name="android:windowLightStatusBar">false</item>
</style>

<style name="SalesforceSDK.ScreenLock.ErrorMessage">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This style was also orphaned when the XML layout for screen lock was removed.

import com.salesforce.androidsdk.ui.components.ScreenLockView
import com.salesforce.androidsdk.util.test.ExcludeFromJacocoGeneratedReport

@ExcludeFromJacocoGeneratedReport
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At a suggestion from our tooling, I moved the preview to a separate source. There were some proposed benefits, though I later realized those didn't manifest. That said, it really did look nice having this non-release code extracted so I left it that way.

To exclude this from code coverage, one suggestion was to move previews to the debug source root. However, I then realized we're building and generating coverage from the debug variant anyways. So, that became a moot point. It was interesting to note that the ExcludeFromJacocoGeneratedReport annotation only applies to the method and not to the default parameter values. That was when I started looking for a way to exclude the entire source.

For now, this seems fine. Perhaps we revisit the variants and directory layout in the future.

innerPadding = PaddingValues.Absolute(0.dp),
appIcon = painterResource(id = R.drawable.sf__salesforce_logo),
viewModel = ScreenLockViewModel(),
logoutAction = ::noOp,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was logout = {} for a time, but of course JaCoCo flagged the anonymous function as uncovered. So, I created a utility noOp function that is trivially tested elsewhere and eliminates the coverage noise. It seemed like an easy win.

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui branch from 44c5b58 to 70ae413 Compare February 7, 2026 00:05
*/
@Composable
internal fun ScreenLockView(
appIcon: Painter,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Painter reference here is a little interesting, but it was a way to pass in the programmatically derived drawable instance for the app icon which doesn't actually have a resource id.

appName: String,
innerPadding: PaddingValues,
logoutAction: () -> Unit,
viewModel: ScreenLockViewModel,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, trailing commas everywhere to reduce future LoC diff.

// Log out button.
Button(
colors = ButtonColors(
containerColor = Transparent,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I based all the colors, fonts and padding from other recently added Compose layouts.

modifier = Modifier.padding(PADDING_SIZE.dp),
onClick = logoutAction,
shape = RoundedCornerShape(CORNER_RADIUS.dp),
) { Text(color = colorScheme.primary, fontWeight = Normal, fontSize = 17.sp, text = stringResource(sf__logout)) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For Compose function call sites that JaCoCo doesn't probe well, I did make them a single line to reduce the impact noise on the repo's stats.

Text(
fontSize = 14.sp,
modifier = Modifier.align(CenterHorizontally),
text = viewModel.setupMessageText.value ?: stringResource(sf__screen_lock_setup_required, appName),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The setup message and setup button label became the best justification for keeping the view model, in the end. They're dynamic.

@@ -1,3 +1,29 @@
/*
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the copyright header here since it was missing. We do want those everywhere, correct?

EdgeToEdge.enable(this);

// Protect against screenshots.
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I carefully migrated all the logic from the previous activity line-by-line and method-by-method to the new activity in a best attempt to avoid missing anything.

@Override
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
create()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I really wanted to be able to pull the data levers on the create logic for top-notch onCreate unit tests and coverage, but being a lifecycle method there's no opening to it. So, I moved all the logic to a private/VisibleForTest method with parameters. That really worked like a charm, I thought.


/**
* Implements the creation of the activity.
* @param build The Android SDK build. This parameter is intended for
Copy link
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce Feb 7, 2026

Choose a reason for hiding this comment

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

The commentary is verbose, yes, but being professional open source I'm really hoping it helps others know the intent and not abuse the contract of the code. I have often felt it is difficult to interpret the idea behind much of the code in the library, so this could help.

@SuppressLint("NewApi")
@VisibleForTesting
internal fun create(
build: Int = SDK_INT,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This activity has a lot of SDK-dependent logic, so I added testable parameters where needed. The bad thing is that it also requires a "suppress". Used with discipline, it seems alright and gets that path automatically tested. I'd be open to hear other ideas on that, though.

internal fun create(
build: Int = SDK_INT,
packageManager: PackageManager = this.packageManager,
sdkManager: SalesforceSDKManager = getInstance(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's too big a topic for a pull request thread, but I'll go on a limb and wonder if this level of testability might require a dependency injection tool like Hilt. I used it quite a bit and it was a love 'n hate experience. It automates this type of test set up a lot, though. It could a great retro topic after this pull request.

build: Int = SDK_INT,
) {
when (biometricManager.canAuthenticate(viewModel.biometricAuthenticators())) {
BIOMETRIC_ERROR_NO_HARDWARE, BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, BIOMETRIC_ERROR_UNSUPPORTED, BIOMETRIC_STATUS_UNKNOWN -> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These codes are not a type safe enumeration. Should we just make this an else so it cannot fall through? @brandonpage, thoughts?

/**
* An empty function.
*/
internal fun noOp() {}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's a dandy, code-covered no-op function (which Kotlin doesn't provide) so we can reduce JaCoCo noise. Perhaps this could live in the new UserInterfaceConstants.kt or elsewhere?

* SalesforceSDKManager instance
* @return The displayed name of the app
*/
fun appName(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This became more testable as a function than a computed property.

* @param build The Android SDK build. This parameter is intended for
* testing purposes only. Defaults to the current Android SDK build
*/
fun biometricAuthenticators(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This also became more testable as a function than a computed property.

<uses-permission android:name="android.permission.USE_BIOMETRIC" />

<application>
<application android:supportsRtl="true">
Copy link
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce Feb 7, 2026

Choose a reason for hiding this comment

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

Wow, so after the Jetpack Compose migration this was actually the only task left. Note, without this the app gets into a strange state where it will use the RTL text but used the LTR layout. It looks really odd.

I reckon this will resource-merge through to the applications and in this case that means all application modules that depend on SalesforceSDK. Is that acceptable? Would we want to leave this off here and let the app modules take responsibility for adding it? Is there a case where an app module could be negatively affected? They could override as needed. I always consider that when updating a library module such as this.

api("androidx.browser:browser:1.8.0") // Update requires API 36 compileSdk
api("androidx.work:work-runtime-ktx:2.10.3")

implementation("com.google.accompanist:accompanist-drawablepainter:0.37.3")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's some Google code that lets us "remember" the programmatically-derived Drawable for the app icon.

assertTrue(sendAccessibilityCapturingSlot.captured.contains(activity.getString(sf__screen_lock_auth_failed)))
}

private fun Drawable.renderToBitmap(): Bitmap {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For asserting the content of the app icon, here's a utility method that helps.

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-19368426_msdk-rewrite-android-screen-lock_a.k.a-passcode_for-rtl-ui branch from 70ae413 to 33fc0b1 Compare February 7, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants