fix(YouTube - Settings): Resolve settings search crash when searching for specific words (#6231)

This commit is contained in:
MarcaD
2025-11-04 14:59:43 +02:00
committed by GitHub
parent e4f52343c0
commit 76dcfaefd8
5 changed files with 74 additions and 17 deletions

View File

@@ -45,6 +45,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.text.Bidi; import java.text.Bidi;
import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -79,6 +81,15 @@ public class Utils {
@Nullable @Nullable
private static Boolean isDarkModeEnabled; private static Boolean isDarkModeEnabled;
// Cached Collator instance with its locale.
@Nullable
private static Locale cachedCollatorLocale;
@Nullable
private static Collator cachedCollator;
private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{P}+");
private static final Pattern DIACRITICS_PATTERN = Pattern.compile("\\p{M}");
private Utils() { private Utils() {
} // utility class } // utility class
@@ -976,30 +987,60 @@ public class Utils {
} }
} }
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
/** /**
* Strips all punctuation and converts to lower case. A null parameter returns an empty string. * Removes punctuation and converts text to lowercase. Returns an empty string if input is null.
*/ */
public static String removePunctuationToLowercase(@Nullable CharSequence original) { public static String removePunctuationToLowercase(@Nullable CharSequence original) {
if (original == null) return ""; if (original == null) return "";
return punctuationPattern.matcher(original).replaceAll("") return PUNCTUATION_PATTERN.matcher(original).replaceAll("")
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale()); .toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
} }
/** /**
* Sort a PreferenceGroup and all it's sub groups by title or key. * Normalizes text for search: applies NFD, removes diacritics, and lowercases (locale-neutral).
* Returns an empty string if input is null.
*/
public static String normalizeTextToLowercase(@Nullable CharSequence original) {
if (original == null) return "";
return DIACRITICS_PATTERN.matcher(Normalizer.normalize(original, Normalizer.Form.NFD))
.replaceAll("").toLowerCase(Locale.ROOT);
}
/**
* Returns a cached Collator for the current locale, or creates a new one if locale changed.
*/
private static Collator getCollator() {
Locale currentLocale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
if (cachedCollator == null || !currentLocale.equals(cachedCollatorLocale)) {
cachedCollatorLocale = currentLocale;
cachedCollator = Collator.getInstance(currentLocale);
cachedCollator.setStrength(Collator.SECONDARY); // Case-insensitive, diacritic-insensitive.
}
return cachedCollator;
}
/**
* Sorts a {@link PreferenceGroup} and all nested subgroups by title or key.
* <p> * <p>
* Sort order is determined by the preferences key {@link Sort} suffix. * The sort order is controlled by the {@link Sort} suffix present in the preference key.
* Preferences without a key or without a {@link Sort} suffix remain in their original order.
* <p> * <p>
* If a preference has no key or no {@link Sort} suffix, * Sorting is performed using {@link Collator} with the current user locale,
* then the preferences are left unsorted. * ensuring correct alphabetical ordering for all supported languages
* (e.g., Ukrainian "і", German "ß", French accented characters, etc.).
*
* @param group the {@link PreferenceGroup} to sort
*/ */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public static void sortPreferenceGroups(PreferenceGroup group) { public static void sortPreferenceGroups(PreferenceGroup group) {
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
List<Pair<String, Preference>> preferences = new ArrayList<>(); List<Pair<String, Preference>> preferences = new ArrayList<>();
// Get cached Collator for locale-aware string comparison.
Collator collator = getCollator();
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference preference = group.getPreference(i); Preference preference = group.getPreference(i);
@@ -1030,10 +1071,11 @@ public class Utils {
preferences.add(new Pair<>(sortValue, preference)); preferences.add(new Pair<>(sortValue, preference));
} }
//noinspection ComparatorCombinators // Sort the list using locale-specific collation rules.
Collections.sort(preferences, (pair1, pair2) Collections.sort(preferences, (pair1, pair2)
-> pair1.first.compareTo(pair2.first)); -> collator.compare(pair1.first, pair2.first));
// Reassign order values to reflect the new sorted sequence
int index = 0; int index = 0;
for (Pair<String, Preference> pair : preferences) { for (Pair<String, Preference> pair : preferences) {
int order = index++; int order = index++;

View File

@@ -392,10 +392,13 @@ public abstract class Setting<T> {
/** /**
* Get the parent Settings that this setting depends on. * Get the parent Settings that this setting depends on.
* @return List of parent Settings (e.g., BooleanSetting or EnumSetting), or empty list if no dependencies exist. * @return List of parent Settings, or empty list if no dependencies exist.
* Defensive: handles null availability or missing getParentSettings() override.
*/ */
public List<Setting<?>> getParentSettings() { public List<Setting<?>> getParentSettings() {
return availability == null ? Collections.emptyList() : availability.getParentSettings(); return availability == null
? Collections.emptyList()
: Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList());
} }
/** /**

View File

@@ -75,7 +75,7 @@ public abstract class BaseSearchResultItem {
// Shared method for highlighting text with search query. // Shared method for highlighting text with search query.
protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) { protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
if (TextUtils.isEmpty(text)) return text; if (TextUtils.isEmpty(text) || queryPattern == null) return text;
final int adjustedColor = Utils.adjustColorBrightness( final int adjustedColor = Utils.adjustColorBrightness(
Utils.getAppBackgroundColor(), 0.95f, 1.20f); Utils.getAppBackgroundColor(), 0.95f, 1.20f);
@@ -84,7 +84,10 @@ public abstract class BaseSearchResultItem {
Matcher matcher = queryPattern.matcher(text); Matcher matcher = queryPattern.matcher(text);
while (matcher.find()) { while (matcher.find()) {
spannable.setSpan(highlightSpan, matcher.start(), matcher.end(), int start = matcher.start();
int end = matcher.end();
if (start == end) continue; // Skip zero matches.
spannable.setSpan(highlightSpan, start, end,
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
@@ -224,10 +227,14 @@ public abstract class BaseSearchResultItem {
return searchBuilder.toString(); return searchBuilder.toString();
} }
/**
* Appends normalized searchable text to the builder.
* Uses full Unicode normalization for accurate search across all languages.
*/
private void appendText(StringBuilder builder, CharSequence text) { private void appendText(StringBuilder builder, CharSequence text) {
if (!TextUtils.isEmpty(text)) { if (!TextUtils.isEmpty(text)) {
if (builder.length() > 0) builder.append(" "); if (builder.length() > 0) builder.append(" ");
builder.append(Utils.removePunctuationToLowercase(text)); builder.append(Utils.normalizeTextToLowercase(text));
} }
} }
@@ -272,7 +279,7 @@ public abstract class BaseSearchResultItem {
*/ */
@Override @Override
boolean matchesQuery(String query) { boolean matchesQuery(String query) {
return searchableText.contains(Utils.removePunctuationToLowercase(query)); return searchableText.contains(Utils.normalizeTextToLowercase(query));
} }
/** /**

View File

@@ -450,7 +450,7 @@ public abstract class BaseSearchViewController {
filteredSearchItems.clear(); filteredSearchItems.clear();
String queryLower = Utils.removePunctuationToLowercase(query); String queryLower = Utils.normalizeTextToLowercase(query);
Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE); Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
// Clear highlighting only for items that were previously visible. // Clear highlighting only for items that were previously visible.

View File

@@ -22,6 +22,11 @@ public class SpoofVideoStreamsPatch {
return Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.isAvailable() return Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.isAvailable()
&& Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32; && Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ANDROID_VR_1_43_32;
} }
@Override
public List<Setting<?>> getParentSettings() {
return List.of(Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE);
}
} }
/** /**