Skip to content

Settings Screen: Java/XML to Kotlin/Compose migration + tests#101

Open
m4pl wants to merge 11 commits intoGrapheneOS:mainfrom
m4pl:appsettings-compose
Open

Settings Screen: Java/XML to Kotlin/Compose migration + tests#101
m4pl wants to merge 11 commits intoGrapheneOS:mainfrom
m4pl:appsettings-compose

Conversation

@m4pl
Copy link
Copy Markdown

@m4pl m4pl commented Apr 3, 2026

Migrated the Settings screen from Java + XML to Kotlin + Compose. Added unit tests for Composable(s) and ViewModel. Additionally, wrote an E2E test covering the flow: user opens Conversations list screen and navigates to a conversation.

Screenshots

Before After
Screenshot_20260403-202830 Screenshot_20260403-202659
Screenshot_20260403-202834 Screenshot_20260403-202702
Screenshot_20260403-202838 Screenshot_20260403-202706

Figma

https://figma.com/design/JtQuS3wQQs9YdwrtZtGw0y

@m4pl m4pl force-pushed the appsettings-compose branch 2 times, most recently from 38013d9 to 05688c9 Compare April 3, 2026 22:40
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be ViewModelComponent probably

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Right, will fix

@m4pl m4pl force-pushed the appsettings-compose branch from 50c7726 to af392d1 Compare April 6, 2026 23:31
@m4pl m4pl force-pushed the appsettings-compose branch from af392d1 to 90cc047 Compare April 6, 2026 23:51
Comment on lines +152 to +162
is SettingsNavRoute.SubscriptionSettings -> {
val sub = uiState.subscriptionSettings.find { it.subId == route.subId }
if (sub != null) {
SubscriptionSettingsScreen(
subscriptionSettings = sub,
title = route.title,
screenModel = screenModel,
onNavigateBack = navigateUp,
)
}
}
Copy link
Copy Markdown
Member

@inthewaves inthewaves Apr 7, 2026

Choose a reason for hiding this comment

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

On a dual-SIM setup, if looking at settings for a SIM and that SIM is removed (eSIM is disabled or physical SIM is removed), sub will be null. Since nothing is used for sub == null case, the entire screen turns white until back gesture.

We should show something (e.g. make SubscriptionSettingsScreen accept a nullable subscriptionSettings and handle it inside) or force the user to go back when SIM is removed (i.e., when sub is null) while SubscriptionSettingsScreen is currently being used (and then add a test for this case)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, this definitely needs to be handled

Comment on lines +86 to +90
val displayPhoneNumber = when {
!savedPhoneNumber.isNullOrEmpty() -> phoneUtils.formatForDisplay(savedPhoneNumber)
!defaultPhoneNumber.isNullOrEmpty() -> phoneUtils.formatForDisplay(defaultPhoneNumber)
else -> context.getString(R.string.unknown_phone_number_pref_display_value)
}
Copy link
Copy Markdown
Member

@inthewaves inthewaves Apr 7, 2026

Choose a reason for hiding this comment

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

Current code only uses the equivalent of PhoneUtils.get(mSubId).formatForDisplay(value)

Previous code used PhoneUtils.get(mSubId).formatForDisplay(value) then bidiFormatter.unicodeWrap(displayValue, TextDirectionHeuristicsCompat.LTR); for phone number preference summary.

@Nullable
@Override
public CharSequence getSummary() {
// Show the preference value if it's set, or the default number if not.
// If we don't have a default, fall back to a static string (e.g. Unknown).
String value = getText();
if (TextUtils.isEmpty(value)) {
value = mDefaultPhoneNumber;
}
final String displayValue = (!TextUtils.isEmpty(value))
? PhoneUtils.get(mSubId).formatForDisplay(value)
: getContext().getString(R.string.unknown_phone_number_pref_display_value);
final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
return bidiFormatter.unicodeWrap
(displayValue, TextDirectionHeuristicsCompat.LTR);
}

It seems we should add this back. There's also the Compose-equivalent of this like TextStyle(textDirection = TextDirection.ContentOrLtr) for phone number Text or OutlinedTextFields. The BidiFormatter usage seems easier/more unconditional though + it would help if string is embedded somewhere else.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, I’ll take another look

Comment on lines +112 to +115
Switch(
checked = checked,
onCheckedChange = null,
)
Copy link
Copy Markdown
Member

@inthewaves inthewaves Apr 7, 2026

Choose a reason for hiding this comment

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

the enabled flag should be passed to the switch. Currently the switch is not dimmed when the SettingsSwitchItem's enabled is false

e.g., disable the Auto-retrieve preference and look at the switch for Roaming auto-retrieve, especially when Roaming auto-retrieve is on

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, I’ll update it so it is less confusing for users

)
}

items(subscriptions) { subscription ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
items(subscriptions) { subscription ->
items(subscriptions, key = { it.subId }) { subscription ->

Comment on lines +29 to +40
@Before
fun setUpDefaultSmsApp() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val packageName = instrumentation.targetContext.packageName
val command = "cmd role add-role-holder android.app.role.SMS $packageName"
val parcelFileDescriptor = instrumentation.uiAutomation.executeShellCommand(command)

ParcelFileDescriptor.AutoCloseInputStream(parcelFileDescriptor).use { inputStream ->
val result = String(inputStream.readBytes())
println("Role assignment result: $result")
}
}
Copy link
Copy Markdown
Member

@inthewaves inthewaves Apr 7, 2026

Choose a reason for hiding this comment

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

This seems like it could be in a @BeforeClass as well (if more tests are added, it'll run for every test)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, that is a good idea for future tests. I’ll update it

Comment thread AndroidManifest.xml
Comment on lines 165 to 169
<activity
android:name=".ui.appsettings.SettingsActivity"
android:label="@string/settings_activity_title"
android:theme="@style/BugleTheme.SettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="user"
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.android.messaging.ui.conversationlist.ConversationListActivity" />
</activity>

<activity
android:name=".ui.appsettings.PerSubscriptionSettingsActivity"
android:label="@string/advanced_settings_activity_title"
android:theme="@style/BugleTheme.SettingsActivity"
android:allowEmbedded="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="user"
android:parentActivityName="com.android.messaging.ui.appsettings.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.android.messaging.ui.appsettings.SettingsActivity" />
</activity>

<activity
android:name=".ui.appsettings.ApplicationSettingsActivity"
android:exported="true"
Copy link
Copy Markdown
Member

@inthewaves inthewaves Apr 7, 2026

Choose a reason for hiding this comment

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

Technically, this is removing the exported Activity .ui.appsettings.ApplicationSettingsActivity and just using .ui.appsettings.SettingsActivity now.

Since .ui.appsettings.ApplicationSettingsActivity used to be exported, for completeness, maybe an activity-alias could be added here for .ui.appsettings.ApplicationSettingsActivity with android:targetActivity=".ui.appsettings.SettingsActivity">

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I will take a look, thanks

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.

4 participants