Skip to content
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ android {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}

androidResources {
generateLocaleConfig = true
}

Comment on lines +100 to +103
Copy link
Member

Choose a reason for hiding this comment

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

If we don't pay attention to new languages being added from Weblate, this might lead to half-translated being proposed to the user. Currently the list of languages is manually kept in settings_keys.xml, so when new languages are added through Weblate they don't automatically appear in the app menu. However, I'm fine with switching to the new behavior, as it would avoid work to keep the language list up to date. Is it possible to access the locale config from within the app, so we can delete the list of languages in settings_keys.xml and use the autogenerated locale config even for the pre-Android-13 language picker?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found LocaleManager#getOverrideLocaleConfig but it sounds like that method is only to get a LocaleConfig that's already been set with setOverrideLocaleConfig(). As far as I can tell, we would have to manually generate a LocaleConfig instead of using the auto-generated one if we wanted to use it for this.

Copy link
Member

Choose a reason for hiding this comment

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

I could find the data under app/build/generated/res/localeConfig/debug/xml/_generated_res_locale_config.xml after running the build, with this data inside:

<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="en-US"/>
    <locale android:name="ace"/>
    ...

So in theory the data is there, but in practice I think we better not access it by manually opening the xml since it's not officially supported. So yeah let's keep the previous behavior.

buildFeatures {
viewBinding true
buildConfig true
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/schabi/newpipe/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ protected void onCreate(final Bundle savedInstanceState) {
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}

Localization.migrateAppLanguageSettingIfNecessary(getApplicationContext());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,12 +22,11 @@
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
Expand All @@ -31,12 +35,28 @@ public void onCreatePreferences(final Bundle savedInstanceState, final String ro

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);
Comment on lines +52 to +54
Copy link
Member

Choose a reason for hiding this comment

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

That's a very edge case, but if the user has disabled settings or if some ROMs are broken (hello weird Android TV devices), this would crash the app due to no app being able to handle the intent.

return true;
});
newAppLanguagePref.setVisible(true);
}

final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
imageQualityPreference.setOnPreferenceChangeListener(
(preference, newValue) -> {
Expand Down Expand Up @@ -72,19 +92,21 @@ public boolean onPreferenceTreeClick(final Preference preference) {
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);
}
}
Expand Down
37 changes: 37 additions & 0 deletions app/src/main/java/org/schabi/newpipe/util/Localization.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
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;
Expand All @@ -39,6 +42,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;


Expand All @@ -63,6 +67,7 @@
*/

public final class Localization {
private static final String TAG = Localization.class.toString();
public static final String DOT_SEPARATOR = " • ";
private static PrettyTime prettyTime;

Expand Down Expand Up @@ -101,6 +106,10 @@ public static Locale getPreferredLocale(@NonNull final Context context) {
}

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

Expand Down Expand Up @@ -427,4 +436,32 @@ private static String getQuantity(@NonNull final Context context,
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"
);
Comment on lines +459 to +461
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the error be logged?

}
}
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/resources.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unqualifiedResLocale=en-US
1 change: 1 addition & 0 deletions app/src/main/res/values/settings_keys.xml
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@
<string name="playback_skip_silence_key">playback_skip_silence_key</string>

<string name="app_language_key">app_language_key</string>
<string name="app_language_android_13_and_up_key">app_language_android_13_and_up_key</string>

<string name="feed_update_threshold_key">feed_update_threshold_key</string>
<string name="feed_update_threshold_default_value">300</string>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/xml/content_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />

<Preference
android:key="@string/app_language_android_13_and_up_key"
android:title="@string/app_language_title"
app:isPreferenceVisible="false"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
Comment on lines +16 to +21
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a separate Preference for the Android 13+ behavior only because for some reason when I set an OnPreferenceClickListener on the existing preference that returns true (indicating "the click was handled"), it still opens the existing dialog.


<ListPreference
android:defaultValue="@string/default_localization_key"
android:entries="@array/language_names"
Expand Down