From 893a227ab169782a539d86b2bb90602f2d2f281f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 19 Jul 2025 19:14:38 +0530 Subject: [PATCH] Enable per-app language preferences for Android < 13 --- app/src/main/AndroidManifest.xml | 9 ++ app/src/main/java/org/schabi/newpipe/App.java | 2 +- .../java/org/schabi/newpipe/MainActivity.java | 8 +- .../org/schabi/newpipe/RouterActivity.java | 2 - .../org/schabi/newpipe/about/AboutActivity.kt | 2 - .../schabi/newpipe/about/LicenseFragment.kt | 2 - .../newpipe/download/DownloadActivity.java | 3 - .../newpipe/download/DownloadDialog.java | 2 - .../schabi/newpipe/error/ErrorActivity.java | 5 +- .../fragments/detail/DescriptionFragment.java | 2 +- .../list/channel/ChannelAboutFragment.java | 4 +- .../ImportConfirmationDialog.java | 3 - .../newpipe/player/PlayQueueActivity.java | 2 - .../org/schabi/newpipe/player/Player.java | 6 +- .../schabi/newpipe/player/PlayerService.java | 3 - .../helper/PlaybackParameterDialog.java | 2 - .../MediaBrowserPlaybackPreparer.kt | 5 +- .../BackupRestoreSettingsFragment.java | 3 - .../settings/ContentSettingsFragment.java | 49 ++++------ .../settings/DownloadSettingsFragment.java | 4 - .../newpipe/settings/SettingsActivity.java | 4 - .../org/schabi/newpipe/util/ListHelper.java | 10 +- .../org/schabi/newpipe/util/Localization.java | 98 ++++++++----------- 23 files changed, 85 insertions(+), 145 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e52dded5e..e0abd977b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,15 @@ + + + + = 33) { - requirePreference(R.string.app_language_key).setVisible(false); - final Preference newAppLanguagePref = + appLanguagePref.setVisible(false); + final var newAppLanguagePref = requirePreference(R.string.app_language_android_13_and_up_key); newAppLanguagePref.setSummaryProvider(preference -> { - final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0); - if (customLocale != null) { - return customLocale.getDisplayName(); - } - return getString(R.string.systems_language); + final Locale loc = AppCompatDelegate.getApplicationLocales().get(0); + return loc != null ? loc.getDisplayName() : getString(R.string.systems_language); }); newAppLanguagePref.setOnPreferenceClickListener(preference -> { final Intent intent = new Intent(Settings.ACTION_APP_LOCALE_SETTINGS) @@ -55,10 +50,16 @@ public class ContentSettingsFragment extends BasePreferenceFragment { return true; }); newAppLanguagePref.setVisible(true); + } else { + appLanguagePref.setOnPreferenceChangeListener((preference, newValue) -> { + final String language = (String) newValue; + final Locale locale = Locale.forLanguageTag(language); + AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(locale)); + return true; + }); } - final Preference imageQualityPreference = requirePreference(R.string.image_quality_key); - imageQualityPreference.setOnPreferenceChangeListener( + requirePreference(R.string.image_quality_key).setOnPreferenceChangeListener( (preference, newValue) -> { ImageStrategy.setPreferredImageQuality(PreferredImageQuality .fromPreferenceKey(requireContext(), (String) newValue)); @@ -92,22 +93,10 @@ public class ContentSettingsFragment extends BasePreferenceFragment { public void onDestroy() { super.onDestroy(); - final String selectedLanguage = - defaultPreferences.getString(getString(R.string.app_language_key), "en"); - - if (!selectedLanguage.equals(initialLanguage)) { - if (Build.VERSION.SDK_INT < 33) { - Toast.makeText( - requireContext(), - R.string.localization_changes_requires_app_restart, - Toast.LENGTH_LONG - ).show(); - } - final Localization selectedLocalization = org.schabi.newpipe.util.Localization - .getPreferredLocalization(requireContext()); - final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization - .getPreferredContentCountry(requireContext()); - NewPipe.setupLocalization(selectedLocalization, selectedContentCountry); - } + final Localization selectedLocalization = org.schabi.newpipe.util.Localization + .getPreferredLocalization(requireContext()); + final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization + .getPreferredContentCountry(requireContext()); + NewPipe.setupLocalization(selectedLocalization, selectedContentCountry); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index ff7811af3..356dcd9b2 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.settings; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - import android.app.Activity; import android.content.ContentResolver; import android.content.Context; @@ -209,8 +207,6 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } private void requestDownloadPathResult(final ActivityResult result, final String key) { - assureCorrectAppLanguage(getContext()); - if (result.getResultCode() != Activity.RESULT_OK) { return; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 0d57ce174..d5089cb7d 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.settings; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - import android.content.Context; import android.os.Bundle; import android.text.TextUtils; @@ -89,7 +87,6 @@ public class SettingsActivity extends AppCompatActivity implements @Override protected void onCreate(final Bundle savedInstanceBundle) { setTheme(ThemeHelper.getSettingsThemeStyle(this)); - assureCorrectAppLanguage(this); super.onCreate(savedInstanceBundle); Bridge.restoreInstanceState(this, savedInstanceBundle); @@ -228,7 +225,6 @@ public class SettingsActivity extends AppCompatActivity implements // Build search items final Context searchContext = getApplicationContext(); - assureCorrectAppLanguage(searchContext); final PreferenceParser parser = new PreferenceParser(searchContext, config); final PreferenceSearcher searcher = new PreferenceSearcher(config); diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 282a88b1e..ea41f3e81 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -322,7 +322,7 @@ public final class ListHelper { } // Sort collected streams by name - return collectedStreams.values().stream().sorted(getAudioTrackNameComparator(context)) + return collectedStreams.values().stream().sorted(getAudioTrackNameComparator()) .collect(Collectors.toList()); } @@ -359,7 +359,7 @@ public final class ListHelper { } // Sort tracks alphabetically, sort track streams by quality - final Comparator nameCmp = getAudioTrackNameComparator(context); + final Comparator nameCmp = getAudioTrackNameComparator(); final Comparator formatCmp = getAudioFormatComparator(context); return collectedStreams.values().stream() @@ -867,12 +867,10 @@ public final class ListHelper { * Get a {@link Comparator} to compare {@link AudioStream}s by their languages and track types * for alphabetical sorting. * - * @param context app context for localization * @return Comparator */ - private static Comparator getAudioTrackNameComparator( - @NonNull final Context context) { - final Locale appLoc = Localization.getAppLocale(context); + private static Comparator getAudioTrackNameComparator() { + final Locale appLoc = Localization.getAppLocale(); return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast( Comparator.comparing(locale -> locale.getDisplayName(appLoc)))) diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 65cfec930..40c7b2a03 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -5,14 +5,12 @@ import static org.schabi.newpipe.MainActivity.DEBUG; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.content.res.Resources; import android.icu.text.CompactDecimalFormat; import android.os.Build; +import android.text.BidiFormatter; import android.text.TextUtils; import android.text.format.DateUtils; -import android.text.BidiFormatter; -import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.NonNull; @@ -43,7 +41,6 @@ import java.time.format.FormatStyle; import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.stream.Collectors; @@ -120,39 +117,35 @@ public final class Localization { return getLocaleFromPrefs(context, R.string.content_language_key); } - public static Locale getAppLocale(@NonNull final Context context) { - if (Build.VERSION.SDK_INT >= 33) { - final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0); - return Objects.requireNonNullElseGet(customLocale, Locale::getDefault); - } - return getLocaleFromPrefs(context, R.string.app_language_key); + public static Locale getAppLocale() { + final Locale customLocale = AppCompatDelegate.getApplicationLocales().get(0); + return customLocale != null ? customLocale : Locale.getDefault(); } - public static String localizeNumber(@NonNull final Context context, final long number) { - return localizeNumber(context, (double) number); + public static String localizeNumber(final long number) { + return localizeNumber((double) number); } - public static String localizeNumber(@NonNull final Context context, final double number) { - final NumberFormat nf = NumberFormat.getInstance(getAppLocale(context)); + public static String localizeNumber(final double number) { + final NumberFormat nf = NumberFormat.getInstance(getAppLocale()); return nf.format(number); } - public static String formatDate(@NonNull final Context context, - @NonNull final OffsetDateTime offsetDateTime) { + public static String formatDate(@NonNull final OffsetDateTime offsetDateTime) { return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(getAppLocale(context)).format(offsetDateTime + .withLocale(getAppLocale()).format(offsetDateTime .atZoneSameInstant(ZoneId.systemDefault())); } @SuppressLint("StringFormatInvalid") public static String localizeUploadDate(@NonNull final Context context, @NonNull final OffsetDateTime offsetDateTime) { - return context.getString(R.string.upload_date_text, formatDate(context, offsetDateTime)); + return context.getString(R.string.upload_date_text, formatDate(offsetDateTime)); } public static String localizeViewCount(@NonNull final Context context, final long viewCount) { return getQuantity(context, R.plurals.views, R.string.no_views, viewCount, - localizeNumber(context, viewCount)); + localizeNumber(viewCount)); } public static String localizeStreamCount(@NonNull final Context context, @@ -166,7 +159,7 @@ public final class Localization { return context.getResources().getString(R.string.more_than_100_videos); default: return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, - localizeNumber(context, streamCount)); + localizeNumber(streamCount)); } } @@ -187,27 +180,27 @@ public final class Localization { public static String localizeWatchingCount(@NonNull final Context context, final long watchingCount) { return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount, - localizeNumber(context, watchingCount)); + localizeNumber(watchingCount)); } public static String shortCount(@NonNull final Context context, final long count) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return CompactDecimalFormat.getInstance(getAppLocale(context), + return CompactDecimalFormat.getInstance(getAppLocale(), CompactDecimalFormat.CompactStyle.SHORT).format(count); } final double value = (double) count; if (count >= 1000000000) { - return localizeNumber(context, round(value / 1000000000)) + return localizeNumber(round(value / 1000000000)) + context.getString(R.string.short_billion); } else if (count >= 1000000) { - return localizeNumber(context, round(value / 1000000)) + return localizeNumber(round(value / 1000000)) + context.getString(R.string.short_million); } else if (count >= 1000) { - return localizeNumber(context, round(value / 1000)) + return localizeNumber(round(value / 1000)) + context.getString(R.string.short_thousand); } else { - return localizeNumber(context, value); + return localizeNumber(value); } } @@ -377,8 +370,8 @@ public final class Localization { prettyTime.removeUnit(Decade.class); } - public static PrettyTime resolvePrettyTime(@NonNull final Context context) { - return new PrettyTime(getAppLocale(context)); + public static PrettyTime resolvePrettyTime() { + return new PrettyTime(getAppLocale()); } public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime) { @@ -410,14 +403,6 @@ public final class Localization { } } - public static void assureCorrectAppLanguage(final Context c) { - final Resources res = c.getResources(); - final DisplayMetrics dm = res.getDisplayMetrics(); - final Configuration conf = res.getConfiguration(); - conf.setLocale(getAppLocale(c)); - res.updateConfiguration(conf, dm); - } - private static Locale getLocaleFromPrefs(@NonNull final Context context, @StringRes final int prefKey) { final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); @@ -453,28 +438,29 @@ public final class Localization { } public static void migrateAppLanguageSettingIfNecessary(@NonNull final Context context) { - // Starting with pull request #12093, NewPipe on Android 13+ exclusively uses Android's + // Starting with pull request #12093, NewPipe exclusively uses Android's // public per-app language APIs to read and set the UI language for NewPipe. - // If running on Android 13+, the following code will migrate any existing custom - // app language in SharedPreferences to use the public per-app language APIs instead. - if (Build.VERSION.SDK_INT >= 33) { - final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - final String appLanguageKey = context.getString(R.string.app_language_key); - final String appLanguageValue = sp.getString(appLanguageKey, null); - if (appLanguageValue != null) { + // The following code will migrate any existing custom app language in SharedPreferences to + // use the public per-app language APIs instead. + final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + final String appLanguageKey = context.getString(R.string.app_language_key); + final String appLanguageValue = sp.getString(appLanguageKey, null); + if (appLanguageValue != null) { + // The app language key is used on Android versions < Tiramisu; for more info, see + // ContentSettingsFragment. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { sp.edit().remove(appLanguageKey).apply(); - final String appLanguageDefaultValue = - context.getString(R.string.default_localization_key); - if (!appLanguageValue.equals(appLanguageDefaultValue)) { - try { - AppCompatDelegate.setApplicationLocales( - LocaleListCompat.forLanguageTags(appLanguageValue) - ); - } catch (final RuntimeException e) { - Log.e(TAG, "Failed to migrate previous custom app language " - + "setting to public per-app language APIs" - ); - } + } + final String appLanguageDefaultValue = + context.getString(R.string.default_localization_key); + if (!appLanguageValue.equals(appLanguageDefaultValue)) { + try { + final var locales = LocaleListCompat.forLanguageTags(appLanguageValue); + AppCompatDelegate.setApplicationLocales(locales); + } catch (final RuntimeException e) { + Log.e(TAG, "Failed to migrate previous custom app language " + + "setting to public per-app language APIs" + ); } } }