diff --git a/app/build.gradle b/app/build.gradle index 756d2562d..19f2bf799 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,6 +101,10 @@ android { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } + androidResources { + generateLocaleConfig = true + } + buildFeatures { viewBinding true compose true diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index df38abdc5..95b1f4164 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -186,6 +186,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 fca8c7162..b998b6337 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; @@ -15,13 +20,13 @@ import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.PreferredImageQuality; +import java.util.Locale; + import coil3.SingletonImageLoader; public class ContentSettingsFragment extends BasePreferenceFragment { private String youtubeRestrictedModeEnabledKey; - private Localization initialSelectedLocalization; - private ContentCountry initialSelectedContentCountry; private String initialLanguage; @Override @@ -30,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) -> { @@ -70,19 +91,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 f8e2df99f..5720ed7ad 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); } @@ -422,4 +431,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/java/org/schabi/newpipe/util/potoken/JavaScriptUtil.kt b/app/src/main/java/org/schabi/newpipe/util/potoken/JavaScriptUtil.kt index a9169e2c6..06740a00e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/potoken/JavaScriptUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/util/potoken/JavaScriptUtil.kt @@ -17,7 +17,7 @@ fun parseChallengeData(rawChallengeData: String): String { val descrambled = descramble(scrambled.getString(1)) JsonParser.array().from(descrambled) } else { - scrambled.getArray(1) + scrambled.getArray(0) } val messageId = challengeData.getString(0) diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 77d270c05..9722a9a1f 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -71,6 +71,9 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; +import java.util.Date; +import java.util.Locale; +import java.text.DateFormat; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; @@ -208,11 +211,17 @@ public class MissionAdapter extends Adapter implements Handler.Callb h.pause.setTitle(mission.unknownLength ? R.string.stop : R.string.pause); updateProgress(h); mPendingDownloadsItems.add(h); + + h.date.setText(""); } else { h.progress.setMarquee(false); h.status.setText("100%"); h.progress.setProgress(1.0f); h.size.setText(Utility.formatBytes(item.mission.length)); + + DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()); + Date date = new Date(item.mission.timestamp); + h.date.setText(dateFormat.format(date)); } } @@ -832,6 +841,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb ImageView icon; TextView name; TextView size; + TextView date; ProgressDrawable progress; PopupMenu popupMenu; @@ -862,6 +872,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb name = itemView.findViewById(R.id.item_name); icon = itemView.findViewById(R.id.item_icon); size = itemView.findViewById(R.id.item_size); + date = itemView.findViewById(R.id.item_date); name.setSelected(true); diff --git a/app/src/main/res/layout/mission_item.xml b/app/src/main/res/layout/mission_item.xml index 5338949aa..c864f60f0 100644 --- a/app/src/main/res/layout/mission_item.xml +++ b/app/src/main/res/layout/mission_item.xml @@ -82,6 +82,18 @@ android:textColor="@color/white" android:textSize="12sp" /> + + diff --git a/app/src/main/res/layout/mission_item_linear.xml b/app/src/main/res/layout/mission_item_linear.xml index ce2d1af4b..6288e4759 100644 --- a/app/src/main/res/layout/mission_item_linear.xml +++ b/app/src/main/res/layout/mission_item_linear.xml @@ -62,6 +62,18 @@ android:textSize="12sp" android:textStyle="bold" /> + + 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" /> + +