Skip to content

Commit

Permalink
Experimental per-app language preferences (#21504)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbradbury authored Dec 11, 2024
2 parents 9a541f3 + 31025b6 commit e9f8e66
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 23 deletions.
10 changes: 8 additions & 2 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -637,9 +637,15 @@ tasks.register("printResourceConfigurations") {
}
}

// Copy React Native JavaScript bundle and source map so they can be upload it to the Crash logging
// service during the build process.
android {
// Enable per-app language support
// https://developer.android.com/guide/topics/resources/app-languages
androidResources {
generateLocaleConfig = true
}

// Copy React Native JavaScript bundle and source map so they can be upload it to the Crash logging
// service during the build process.
applicationVariants.configureEach { variant ->
def variantAssets = variant.mergeAssetsProvider.get().outputDir.get()

Expand Down
9 changes: 9 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,15 @@
android:label="Site Creation Service"
android:foregroundServiceType="dataSync" />

<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>

<!-- Samsung multiwindow support -->
<uses-library
android:name="com.sec.android.app.multiwindow"
Expand Down
13 changes: 11 additions & 2 deletions WordPress/src/main/java/org/wordpress/android/AppInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import org.wordpress.android.util.FluxCUtils
import org.wordpress.android.util.LocaleManager
import org.wordpress.android.util.NetworkUtils
import org.wordpress.android.util.PackageUtils
import org.wordpress.android.util.PerAppLocaleManager
import org.wordpress.android.util.ProfilingUtils
import org.wordpress.android.util.QuickStartUtils
import org.wordpress.android.util.RateLimitedTask
Expand Down Expand Up @@ -231,6 +232,9 @@ class AppInitializer @Inject constructor(
@Inject
lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper

@Inject
lateinit var perAppLocaleManager: PerAppLocaleManager

private lateinit var applicationLifecycleMonitor: ApplicationLifecycleMonitor

@Suppress("DEPRECATION")
Expand Down Expand Up @@ -946,8 +950,13 @@ class AppInitializer @Inject constructor(
*/
private inner class MemoryAndConfigChangeMonitor : ComponentCallbacks2 {
override fun onConfigurationChanged(newConfig: Configuration) {
// Reapply locale on configuration change
LocaleManager.setLocale(context)
// If per-app locale is enabled make sure the in-app locale is correct,
// otherwise reapply in-app locale on configuration change
if (perAppLocaleManager.isPerAppLanguagePrefsEnabled()) {
perAppLocaleManager.checkAndUpdateOldLanguagePrefKey()
} else {
LocaleManager.setLocale(context)
}
}

override fun onLowMemory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package org.wordpress.android.ui
import android.content.Context
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatActivity
import org.wordpress.android.ui.prefs.AppPrefs
import org.wordpress.android.util.LocaleManager
import org.wordpress.android.util.PerAppLocaleManager

/**
* Newer versions of the AppCompat library no longer support locale changes at application level,
Expand All @@ -15,19 +17,39 @@ import org.wordpress.android.util.LocaleManager
*
* Note: please be mindful of the principle of favoring composition over inheritance and refrain from
* building upon this class unless it's absolutely necessary.
*
* Update Dec 2024: We've added experimental support for per-app language preferences which
* will eventually negate the need for this class. Instead of extending from this class, we
* should extend from AppCompatActivity once this feature is out of the experimental phase.
*/
abstract class LocaleAwareActivity : AppCompatActivity() {
/**
* Used to update locales on API 21 to API 25.
*/
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(LocaleManager.setLocale(newBase))
if (isPerAppLocaleEnabled()) {
super.attachBaseContext(newBase)
} else {
super.attachBaseContext(LocaleManager.setLocale(newBase))
}
}

/**
* Used to update locales on API 26 and beyond.
*/
override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
super.applyOverrideConfiguration(LocaleManager.updatedConfigLocale(baseContext, overrideConfiguration))
if (isPerAppLocaleEnabled()) {
super.applyOverrideConfiguration(overrideConfiguration)
} else {
super.applyOverrideConfiguration(LocaleManager.updatedConfigLocale(baseContext, overrideConfiguration))
}
}

/**
* Ideally we would use [PerAppLocaleManager.isPerAppLanguagePrefsEnabled] here, but we
* can't inject [PerAppLocaleManager] into an abstract class
*/
private fun isPerAppLocaleEnabled(): Boolean {
return AppPrefs.getManualFeatureConfig(PerAppLocaleManager.EXPERIMENTAL_PER_APP_LANGUAGE_PREF_KEY)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
import org.wordpress.android.ui.prefs.AppSettingsActivity;
import org.wordpress.android.ui.prefs.AppSettingsFragment;
import org.wordpress.android.ui.prefs.SiteSettingsFragment;
import org.wordpress.android.util.PerAppLocaleManager;
import org.wordpress.android.ui.prefs.privacy.banner.PrivacyBannerFragment;
import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts;
import org.wordpress.android.ui.quickstart.QuickStartTracker;
Expand Down Expand Up @@ -303,6 +304,8 @@ public class WPMainActivity extends LocaleAwareActivity implements

@Inject SnackbarSequencer mSnackbarSequencer;

@Inject PerAppLocaleManager mPerAppLocaleManager;

/*
* fragments implement this if their contents can be scrolled, called when user
* requests to scroll to the top
Expand Down Expand Up @@ -364,6 +367,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
InstallationReferrerServiceStarter.startService(this, null);
}

mPerAppLocaleManager.performMigrationIfNecessary();

if (FluxCUtils.isSignedInWPComOrHasWPOrgSite(mAccountStore, mSiteStore)
&& !AppPrefs.getIsJetpackMigrationInProgress()) {
NotificationType notificationType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra
isHidden: Boolean
) = AppPrefs.setShouldHidePostDashboardCard(siteId, postCardType, isHidden)

fun getShouldHidePostDashboardCard(siteId: Long, postCardType: String,): Boolean =
fun getShouldHidePostDashboardCard(siteId: Long, postCardType: String): Boolean =
AppPrefs.getShouldHidePostDashboardCard(siteId, postCardType)

fun setShouldHideNextStepsDashboardCard(siteId: Long, isHidden: Boolean) =
Expand Down Expand Up @@ -442,7 +442,7 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra
fun setShouldHideDynamicCard(id: String, isHidden: Boolean): Unit =
AppPrefs.setShouldHideDynamicCard(id, isHidden)

fun getShouldHideDynamicCard(id: String, ): Boolean =
fun getShouldHideDynamicCard(id: String): Boolean =
AppPrefs.getShouldHideDynamicCard(id)

fun shouldUpdateBookmarkPostsPseudoIds(tag: ReaderTag?): Boolean = AppPrefs.shouldUpdateBookmarkPostsPseudoIds(tag)
Expand All @@ -456,6 +456,14 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra

fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()

fun getPrefString(key: String, defValue: String): String? {
return AppPrefs.prefs().getString(key, defValue)
}

fun setPrefString(key: String, value: String) {
AppPrefs.prefs().edit().putString(key, value).apply()
}

fun getDebugBooleanPref(key: String, default: Boolean = false) =
buildConfigWrapper.isDebug() && AppPrefs.getRawBoolean({ key }, default)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
Expand Down Expand Up @@ -48,6 +49,7 @@
import org.wordpress.android.ui.deeplinks.DeepLinkOpenWebLinksWithJetpackHelper;
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper;
import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment;
import org.wordpress.android.util.PerAppLocaleManager;
import org.wordpress.android.ui.prefs.language.LocalePickerBottomSheet;
import org.wordpress.android.ui.prefs.language.LocalePickerBottomSheet.LocalePickerCallback;
import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic;
Expand Down Expand Up @@ -99,6 +101,7 @@ public class AppSettingsFragment extends PreferenceFragment
private WPSwitchPreference mOpenWebLinksWithJetpack;

private Preference mWhatsNew;
private Boolean mIsPerAppLanguagePrefsEnabled;

@Inject SiteStore mSiteStore;
@Inject AccountStore mAccountStore;
Expand All @@ -111,6 +114,7 @@ public class AppSettingsFragment extends PreferenceFragment
@Inject DeepLinkOpenWebLinksWithJetpackHelper mOpenWebLinksWithJetpackHelper;
@Inject UiHelpers mUiHelpers;
@Inject JetpackFeatureRemovalPhaseHelper mJetpackFeatureRemovalPhaseHelper;
@Inject PerAppLocaleManager mPerAppLocaleManager;

private static final String TRACK_STYLE = "style";
private static final String TRACK_ENABLED = "enabled";
Expand All @@ -121,6 +125,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
((WordPress) getActivity().getApplication()).component().inject(this);
mDispatcher.register(this);

mIsPerAppLanguagePrefsEnabled = mPerAppLocaleManager.isPerAppLanguagePrefsEnabled();

addPreferencesFromResource(R.xml.app_settings);

findPreference(getString(R.string.pref_key_send_usage)).setOnPreferenceChangeListener(
Expand All @@ -144,11 +150,6 @@ public boolean onPreferenceChange(Preference preference, Object newValue) {
);
updateAnalyticsSyncUI();

mLanguagePreference = (WPPreference) findPreference(getString(R.string.pref_key_language));
mLanguagePreference.setOnPreferenceChangeListener(this);
mLanguagePreference.setOnPreferenceClickListener(this);
mLanguagePreference.setSummary(mLocaleProvider.getAppLanguageDisplayString());

mAppThemePreference = (ListPreference) findPreference(getString(R.string.pref_key_app_theme));
mAppThemePreference.setOnPreferenceChangeListener(this);

Expand Down Expand Up @@ -254,6 +255,16 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
ViewCompat.setNestedScrollingEnabled(listOfPreferences, true);
addJetpackBadgeAsFooterIfEnabled(inflater, listOfPreferences);
}

mLanguagePreference = (WPPreference) findPreference(getString(R.string.pref_key_language));
mLanguagePreference.setOnPreferenceChangeListener(this);
mLanguagePreference.setOnPreferenceClickListener(this);
if (mIsPerAppLanguagePrefsEnabled) {
mLanguagePreference.setSummary(mPerAppLocaleManager.getCurrentLocaleDisplayName());
} else {
mLanguagePreference.setSummary(mLocaleProvider.getAppLanguageDisplayString());
}

return view;
}

Expand Down Expand Up @@ -368,6 +379,7 @@ public void onAccountChanged(OnAccountChanged event) {
if (event.isError()) {
switch (event.error.type) {
case SETTINGS_FETCH_GENERIC_ERROR:
case ACCOUNT_FETCH_ERROR:
ToastUtils
.showToast(getActivity(), R.string.error_fetch_account_settings, ToastUtils.Duration.LONG);
break;
Expand All @@ -378,6 +390,10 @@ public void onAccountChanged(OnAccountChanged event) {
case SETTINGS_POST_ERROR:
ToastUtils.showToast(getActivity(), R.string.error_post_account_settings, ToastUtils.Duration.LONG);
break;
case SEND_VERIFICATION_EMAIL_ERROR:
break;
case GENERIC_ERROR:
break;
}
} else if (event.causeOfChange == AccountAction.FETCH_SETTINGS) {
// no need to sync with remote here, or do anything else here, since the logic is already in WordPress.java
Expand Down Expand Up @@ -632,7 +648,12 @@ private boolean handleFeatureAnnouncementClick() {
}

private boolean handleAppLocalePickerClick() {
if (getActivity() instanceof AppCompatActivity) {
// if per-app language preferences are enabled and the device is on API 33+, take the user to the
// system app settings to change the language
if (mIsPerAppLanguagePrefsEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mPerAppLocaleManager.openAppLanguageSettings(getContext());
return true;
} else if (getActivity() instanceof AppCompatActivity) {
LocalePickerBottomSheet bottomSheet = LocalePickerBottomSheet.newInstance();
bottomSheet.setLocalePickerCallback(this);
bottomSheet.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(),
Expand All @@ -657,7 +678,11 @@ private void reattachLocalePickerCallback() {

@Override
public void onLocaleSelected(@NonNull String languageCode) {
onPreferenceChange(mLanguagePreference, languageCode);
if (mIsPerAppLanguagePrefsEnabled) {
mPerAppLocaleManager.setCurrentLocaleByLanguageCode(languageCode);
} else {
onPreferenceChange(mLanguagePreference, languageCode);
}
}

private void handleOpenLinksInJetpack(Boolean newValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -34,26 +32,33 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.wordpress.android.R
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.compose.unit.Margin
import org.wordpress.android.util.PerAppLocaleManager
import org.wordpress.android.util.extensions.setContent
import javax.inject.Inject

val experimentalFeatures = listOf(
Feature(key = "experimental_block_editor"),
Feature(key = "experimental_block_editor_theme_styles")
Feature(key = "experimental_block_editor_theme_styles"),
Feature(key = PerAppLocaleManager.EXPERIMENTAL_PER_APP_LANGUAGE_PREF_KEY)
)

data class Feature(
val enabled: Boolean = false,
val key: String,
)

class FeatureViewModel : ViewModel() {
@HiltViewModel
class FeatureViewModel @Inject constructor(
private val perAppLocaleManager: PerAppLocaleManager
) : ViewModel() {
private val _switchStates = MutableStateFlow<Map<String, Feature>>(emptyMap())
val switchStates: StateFlow<Map<String, Feature>> = _switchStates.asStateFlow()

Expand All @@ -64,13 +69,25 @@ class FeatureViewModel : ViewModel() {
_switchStates.value = initialStates
}

fun toggleFeature(key: String, enabled: Boolean) {
fun onFeatureToggled(key: String, enabled: Boolean) {
_switchStates.update { currentStates ->
currentStates.toMutableMap().apply {
this[key] = Feature(enabled, key)
AppPrefs.setManualFeatureConfig(enabled, key)
}
}

featureToggled(key, enabled)
}

private fun featureToggled(key: String, enabled: Boolean) {
if (key == PerAppLocaleManager.EXPERIMENTAL_PER_APP_LANGUAGE_PREF_KEY) {
if (enabled) {
perAppLocaleManager.performMigrationIfNecessary()
} else {
perAppLocaleManager.resetApplicationLocale()
}
}
}
}

Expand All @@ -87,7 +104,7 @@ class ExperimentalFeaturesActivity : AppCompatActivity() {

ExperimentalFeaturesScreen(
features = features,
onFeatureToggled = viewModel::toggleFeature,
onFeatureToggled = viewModel::onFeatureToggled,
onNavigateBack = onBackPressedDispatcher::onBackPressed
)
}
Expand Down Expand Up @@ -158,13 +175,14 @@ fun FeatureToggle(
.clickable { onChange(key, !enabled) }
.padding(horizontal = Margin.ExtraLarge.value, vertical = Margin.MediumLarge.value)
) {
Spacer(modifier = Modifier.width(Margin.ExtraLarge.value))
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.weight(1f)
.padding(horizontal = Margin.Medium.value)
)
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = enabled,
onCheckedChange = { newValue ->
Expand Down
Loading

0 comments on commit e9f8e66

Please sign in to comment.