Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
import androidx.annotation.Nullable;

import java.text.Bidi;
import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -79,6 +81,15 @@ public class Utils {
@Nullable
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() {
} // utility class

Expand Down Expand Up @@ -976,30 +987,60 @@ static Sort fromKey(@Nullable String key, Sort defaultSort) {
}
}

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) {
if (original == null) return "";
return punctuationPattern.matcher(original).replaceAll("")
return PUNCTUATION_PATTERN.matcher(original).replaceAll("")
.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>
* 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>
* If a preference has no key or no {@link Sort} suffix,
* then the preferences are left unsorted.
* Sorting is performed using {@link Collator} with the current user locale,
* 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")
public static void sortPreferenceGroups(PreferenceGroup group) {
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
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++) {
Preference preference = group.getPreference(i);

Expand Down Expand Up @@ -1030,10 +1071,11 @@ public static void sortPreferenceGroups(PreferenceGroup group) {
preferences.add(new Pair<>(sortValue, preference));
}

//noinspection ComparatorCombinators
// Sort the list using locale-specific collation rules.
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;
for (Pair<String, Preference> pair : preferences) {
int order = index++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,13 @@ public boolean isAvailable() {

/**
* 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() {
return availability == null ? Collections.emptyList() : availability.getParentSettings();
return availability == null
? Collections.emptyList()
: Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ private static int getResourceIdentifier(String name) {

// Shared method for highlighting text with search query.
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(
Utils.getAppBackgroundColor(), 0.95f, 1.20f);
Expand All @@ -84,7 +84,10 @@ protected static CharSequence highlightSearchQuery(CharSequence text, Pattern qu

Matcher matcher = queryPattern.matcher(text);
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);
}

Expand Down Expand Up @@ -224,10 +227,14 @@ private String buildSearchableText(Preference pref) {
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) {
if (!TextUtils.isEmpty(text)) {
if (builder.length() > 0) builder.append(" ");
builder.append(Utils.removePunctuationToLowercase(text));
builder.append(Utils.normalizeTextToLowercase(text));
}
}

Expand Down Expand Up @@ -272,7 +279,7 @@ public CharSequence getCurrentEffectiveSummary() {
*/
@Override
boolean matchesQuery(String query) {
return searchableText.contains(Utils.removePunctuationToLowercase(query));
return searchableText.contains(Utils.normalizeTextToLowercase(query));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ protected void filterAndShowResults(String query) {

filteredSearchItems.clear();

String queryLower = Utils.removePunctuationToLowercase(query);
String queryLower = Utils.normalizeTextToLowercase(query);
Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);

// Clear highlighting only for items that were previously visible.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public boolean isAvailable() {
return Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.isAvailable()
&& 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);
}
}

/**
Expand Down
Loading