Enable per-app language preferences for Android < 13

This commit is contained in:
Isira Seneviratne 2025-07-19 19:14:38 +05:30
parent 0db859e225
commit 893a227ab1
23 changed files with 85 additions and 145 deletions

View file

@ -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<AudioStream> nameCmp = getAudioTrackNameComparator(context);
final Comparator<AudioStream> nameCmp = getAudioTrackNameComparator();
final Comparator<AudioStream> 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<AudioStream> getAudioTrackNameComparator(
@NonNull final Context context) {
final Locale appLoc = Localization.getAppLocale(context);
private static Comparator<AudioStream> getAudioTrackNameComparator() {
final Locale appLoc = Localization.getAppLocale();
return Comparator.comparing(AudioStream::getAudioLocale, Comparator.nullsLast(
Comparator.comparing(locale -> locale.getDisplayName(appLoc))))

View file

@ -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"
);
}
}
}