diff --git a/app/build.gradle b/app/build.gradle index f9b2e9bd9..f3159f741 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,10 @@ android { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } + androidResources { + generateLocaleConfig = true + } + buildFeatures { viewBinding true buildConfig true diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index c4937431f..f74e8b7a7 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -190,6 +190,8 @@ public class MainActivity extends AppCompatActivity { && ReleaseVersionUtil.INSTANCE.isReleaseApk()) { UpdateSettingsFragment.askForConsentToUpdateChecks(this); } + + Localization.migrateAppLanguageSettingIfNecessary(getApplicationContext()); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index ec2bed67a..a8129f680 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -1,10 +1,15 @@ package org.schabi.newpipe.settings; import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.util.Log; import android.widget.Toast; +import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.Preference; import org.schabi.newpipe.DownloaderImpl; @@ -17,12 +22,11 @@ import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PreferredImageQuality; import java.io.IOException; +import java.util.Locale; public class ContentSettingsFragment extends BasePreferenceFragment { private String youtubeRestrictedModeEnabledKey; - private Localization initialSelectedLocalization; - private ContentCountry initialSelectedContentCountry; private String initialLanguage; @Override @@ -31,12 +35,28 @@ public class ContentSettingsFragment extends BasePreferenceFragment { addPreferencesFromResourceRegistry(); - initialSelectedLocalization = org.schabi.newpipe.util.Localization - .getPreferredLocalization(requireContext()); - initialSelectedContentCountry = org.schabi.newpipe.util.Localization - .getPreferredContentCountry(requireContext()); initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en"); + if (Build.VERSION.SDK_INT >= 33) { + requirePreference(R.string.app_language_key).setVisible(false); + final Preference 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); + }); + newAppLanguagePref.setOnPreferenceClickListener(preference -> { + final Intent intent = new Intent(Settings.ACTION_APP_LOCALE_SETTINGS) + .setData(Uri.fromParts("package", requireContext().getPackageName(), null)); + startActivity(intent); + return true; + }); + newAppLanguagePref.setVisible(true); + } + final Preference imageQualityPreference = requirePreference(R.string.image_quality_key); imageQualityPreference.setOnPreferenceChangeListener( (preference, newValue) -> { @@ -72,19 +92,21 @@ public class ContentSettingsFragment extends BasePreferenceFragment { public void onDestroy() { super.onDestroy(); - final Localization selectedLocalization = org.schabi.newpipe.util.Localization - .getPreferredLocalization(requireContext()); - final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization - .getPreferredContentCountry(requireContext()); final String selectedLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en"); - if (!selectedLocalization.equals(initialSelectedLocalization) - || !selectedContentCountry.equals(initialSelectedContentCountry) - || !selectedLanguage.equals(initialLanguage)) { - Toast.makeText(requireContext(), R.string.localization_changes_requires_app_restart, - Toast.LENGTH_LONG).show(); - + 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); } } 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 6830e390b..2146cf8bc 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -12,12 +12,15 @@ import android.os.Build; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.DisplayMetrics; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.PluralsRes; import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatDelegate; import androidx.core.math.MathUtils; +import androidx.core.os.LocaleListCompat; import androidx.preference.PreferenceManager; import org.ocpsoft.prettytime.PrettyTime; @@ -39,6 +42,7 @@ 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; @@ -63,6 +67,7 @@ import java.util.stream.Collectors; */ public final class Localization { + private static final String TAG = Localization.class.toString(); public static final String DOT_SEPARATOR = " • "; private static PrettyTime prettyTime; @@ -101,6 +106,10 @@ public final class Localization { } 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); } @@ -427,4 +436,32 @@ public final class Localization { final int safeCount = (int) MathUtils.clamp(count, Integer.MIN_VALUE, Integer.MAX_VALUE); return context.getResources().getQuantityString(pluralId, safeCount, formattedCount); } + + public static void migrateAppLanguageSettingIfNecessary(@NonNull final Context context) { + // Starting with pull request #12093, NewPipe on Android 13+ 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) { + 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" + ); + } + } + } + } + } } diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 000000000..467b3efec --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index fb68a464d..61125c47f 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -353,6 +353,7 @@ playback_skip_silence_key app_language_key + app_language_android_13_and_up_key feed_update_threshold_key 300 diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index 2cdc6c545..f17783a22 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -13,6 +13,13 @@ app:iconSpaceReserved="false" app:useSimpleSummaryProvider="true" /> + +