chore: Merge branch dev to main (#499)

This commit is contained in:
oSumAtrIX 2023-10-20 03:22:33 +02:00 committed by GitHub
commit bb9b35554f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 448 additions and 49 deletions

View File

@ -1,3 +1,59 @@
# [0.120.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.5...v0.120.0-dev.6) (2023-10-20)
### Features
* **YouTube:** Add `Announcements` patch ([#503](https://github.com/ReVanced/revanced-integrations/issues/503)) ([59687f1](https://github.com/ReVanced/revanced-integrations/commit/59687f1a39768ab71b2680a5c49df5aaae0d3b4c))
# [0.120.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.4...v0.120.0-dev.5) (2023-10-19)
### Features
* **YouTube:** Add `Spoof device dimensions` patch ([16f1163](https://github.com/ReVanced/revanced-integrations/commit/16f1163a346fef0a87ca9384c9bf6aea977dc8fb))
# [0.120.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.3...v0.120.0-dev.4) (2023-10-17)
### Performance Improvements
* **YouTube:** Reduce memory requirement for prefix tree searching ([#501](https://github.com/ReVanced/revanced-integrations/issues/501)) ([f5add51](https://github.com/ReVanced/revanced-integrations/commit/f5add51fa7eb620a6edd1b27f02d38618f144480))
# [0.120.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.2...v0.120.0-dev.3) (2023-10-15)
### Bug Fixes
* **YouTube - Old video quality menu:** Fix toast error on tablet devices ([#500](https://github.com/ReVanced/revanced-integrations/issues/500)) ([d3eba27](https://github.com/ReVanced/revanced-integrations/commit/d3eba27c909e519eaeda072c1f995a2284007749))
# [0.120.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.120.0-dev.1...v0.120.0-dev.2) (2023-10-14)
### Bug Fixes
* **YouTube - Minimized playback:** Fix pip incorrectly showing if app is minimized immediately after opening a Short ([7d02774](https://github.com/ReVanced/revanced-integrations/commit/7d02774ea192510e692e90ae55a86e25ee321926))
# [0.120.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.3-dev.2...v0.120.0-dev.1) (2023-10-13)
### Features
* **YouTube - Theme:** Disable gradient loading screen ([fd09e46](https://github.com/ReVanced/revanced-integrations/commit/fd09e46d01c820632cfe440dac34f5cd957e793d))
## [0.119.3-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.3-dev.1...v0.119.3-dev.2) (2023-10-13)
### Bug Fixes
* **YouTube - Hide Layout components:** Exempt expandable chips from exceptions ([#498](https://github.com/ReVanced/revanced-integrations/issues/498)) ([6f79746](https://github.com/ReVanced/revanced-integrations/commit/6f79746d788f196f3aa63b8e7c24b7f15ecd3f50))
## [0.119.3-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v0.119.2...v0.119.3-dev.1) (2023-10-13)
### Bug Fixes
* **YouTube - Hide layout components:** Hide new channel watermark component ([9670bd3](https://github.com/ReVanced/revanced-integrations/commit/9670bd305b3b9bbbc900af3b64152aaac125ec14))
## [0.119.2](https://github.com/ReVanced/revanced-integrations/compare/v0.119.1...v0.119.2) (2023-10-12)

View File

@ -1,11 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class BrandingWaterMarkPatch {
// Used by: app.revanced.patches.youtube.layout.watermark.patch.HideWatermarkPatch
public static boolean isBrandingWatermarkShown() {
return SettingsEnum.HIDE_VIDEO_WATERMARK.getBoolean() == false;
}
}

View File

@ -5,7 +5,7 @@ import app.revanced.integrations.shared.PlayerType;
public class MinimizedPlaybackPatch {
public static boolean isPlaybackNotShort() {
return !PlayerType.getCurrent().isNoneOrHidden();
return !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
}
public static boolean overrideMinimizedPlaybackAvailable() {

View File

@ -0,0 +1,151 @@
package app.revanced.integrations.patches.announcements;
import android.app.Activity;
import android.os.Build;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.annotation.RequiresApi;
import app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.UUID;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT;
public final class AnnouncementsPatch {
private final static String CONSUMER = getOrSetConsumer();
private AnnouncementsPatch() {
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void showAnnouncement(final Activity context) {
if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return;
ReVancedUtils.runOnBackgroundThread(() -> {
try {
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER);
LogHelper.printDebug(() -> "Get latest announcement route connection url: " + connection.getURL().toString());
try {
// Do not show the announcement if the request failed.
if (connection.getResponseCode() != 200) {
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return;
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
ReVancedUtils.showToastLong("Failed to get announcement");
return;
}
} catch (IOException ex) {
final var message = "Failed connecting to announcements provider";
LogHelper.printException(() -> message, ex);
return;
}
var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false);
// Do not show the announcement if it is older or the same as the last one.
final byte[] hashBytes = MessageDigest.getInstance("SHA-256").digest(jsonString.getBytes(StandardCharsets.UTF_8));
final var hash = java.util.Base64.getEncoder().encodeToString(hashBytes);
if (hash.equals(SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString())) return;
// Parse the announcement. Fall-back to raw string if it fails.
String title;
String message;
Level level = Level.INFO;
try {
final var announcement = new JSONObject(jsonString);
title = announcement.getString("title");
message = announcement.getJSONObject("content").getString("message");
if (!announcement.isNull("level")) level = Level.fromInt(announcement.getInt("level"));
} catch (Throwable ex) {
LogHelper.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
title = "Announcement";
message = jsonString;
}
final var finalTitle = title;
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
final Level finalLevel = level;
ReVancedUtils.runOnMainThread(() -> {
// Show the announcement.
var alertDialog = new android.app.AlertDialog.Builder(context)
.setTitle(finalTitle)
.setMessage(finalMessage)
.setIcon(finalLevel.icon)
.setPositiveButton("Ok", (dialog, which) -> {
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(hash);
dialog.dismiss();
}).setNegativeButton("Dismiss", (dialog, which) -> {
dialog.dismiss();
})
.setCancelable(false)
.show();
// Make links clickable.
((TextView)alertDialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
});
} catch (Exception e) {
final var message = "Failed to get announcement";
LogHelper.printException(() -> message, e);
}
});
}
/**
* Clears the last announcement hash if it is not empty.
*
* @return true if the last announcement hash was empty.
*/
private static boolean emptyLastAnnouncementHash() {
if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true;
SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue("");
return false;
}
private static String getOrSetConsumer() {
final var consumer = SettingsEnum.ANNOUNCEMENT_CONSUMER.getString();
if (!consumer.isEmpty()) return consumer;
final var uuid = UUID.randomUUID().toString();
SettingsEnum.ANNOUNCEMENT_CONSUMER.saveValue(uuid);
return uuid;
}
// TODO: Use better icons.
private enum Level {
INFO(android.R.drawable.ic_dialog_info),
WARNING(android.R.drawable.ic_dialog_alert),
SEVERE(android.R.drawable.ic_dialog_alert);
public final int icon;
Level(int icon) {
this.icon = icon;
}
public static Level fromInt(int value) {
return values()[Math.min(value, values().length - 1)];
}
}
}

View File

@ -0,0 +1,23 @@
package app.revanced.integrations.patches.announcements.requests;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.requests.Route;
import java.io.IOException;
import java.net.HttpURLConnection;
import static app.revanced.integrations.requests.Route.Method.GET;
public class AnnouncementsRoutes {
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v2";
public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?consumer={consumer}");
private AnnouncementsRoutes() {
}
public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params);
}
}

View File

@ -22,6 +22,7 @@ public final class LayoutComponentsFilter extends Filter {
private final StringFilterGroup searchResultShelfHeader;
private final StringFilterGroup inFeedSurvey;
private final StringFilterGroup notifyMe;
private final StringFilterGroup expandableMetadata;
@RequiresApi(api = Build.VERSION_CODES.N)
public LayoutComponentsFilter() {
@ -114,7 +115,7 @@ public final class LayoutComponentsFilter extends Filter {
"official_card"
);
final var expandableMetadata = new StringFilterGroup(
expandableMetadata = new StringFilterGroup(
SettingsEnum.HIDE_EXPANDABLE_CHIP,
"inline_expander"
);
@ -175,11 +176,17 @@ public final class LayoutComponentsFilter extends Filter {
"chips_shelf"
);
final var channelWatermark = new StringFilterGroup(
SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK,
"featured_channel_watermark_overlay"
);
this.pathFilterGroupList.addAll(
channelBar,
communityPosts,
paidContent,
latestPosts,
channelWatermark,
communityGuidelines,
quickActions,
expandableMetadata,
@ -211,7 +218,10 @@ public final class LayoutComponentsFilter extends Filter {
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey) return true;
// The groups are excluded from the filter due to the exceptions list below.
// Filter them separately here.
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
return super.isFiltered(identifier, path, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
if (matchedGroup != custom && exceptions.matches(path))
return false; // Exceptions are not filtered.
@ -225,7 +235,6 @@ public final class LayoutComponentsFilter extends Filter {
/**
* Injection point.
*
* Called from a different place then the other filters.
*/
public static boolean filterMixPlaylists(final byte[] bytes) {
@ -236,4 +245,8 @@ public final class LayoutComponentsFilter extends Filter {
return isMixPlaylistFiltered;
}
public static boolean showWatermark() {
return !SettingsEnum.HIDE_VIDEO_CHANNEL_WATERMARK.getBoolean();
}
}

View File

@ -425,15 +425,15 @@ public final class LithoFilterPatch {
static {
for (Filter filter : filters) {
filterGroupLists(pathSearchTree, filter, filter.pathFilterGroupList);
filterGroupLists(identifierSearchTree, filter, filter.identifierFilterGroupList);
filterGroupLists(pathSearchTree, filter, filter.pathFilterGroupList);
}
LogHelper.printDebug(() -> "Using: "
+ pathSearchTree.numberOfPatterns() + " path filters"
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB), "
+ identifierSearchTree.numberOfPatterns() + " identifier filters"
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB)");
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
+ pathSearchTree.numberOfPatterns() + " path filters"
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
}
private static <T> void filterGroupLists(TrieSearch<T> pathSearchTree,

View File

@ -27,10 +27,13 @@ public final class OldVideoQualityMenuPatch {
// Check if the current view is the quality menu.
if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) {
VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false;
((ViewGroup) recyclerView.getParent().getParent().getParent()).setVisibility(View.GONE);
// Click the "Advanced" quality menu to show the "old" quality menu.
((ViewGroup) recyclerView.getChildAt(0)).getChildAt(3).performClick();
((ViewGroup) recyclerView.getParent().getParent().getParent()).setVisibility(View.GONE);
View advancedQualityView = ((ViewGroup) recyclerView.getChildAt(0)).getChildAt(3);
if (advancedQualityView != null) {
// Click the "Advanced" quality menu to show the "old" quality menu.
advancedQualityView.performClick();
}
}
} catch (Exception ex) {
LogHelper.printException(() -> "onFlyoutMenuCreate failure", ex);

View File

@ -0,0 +1,14 @@
package app.revanced.integrations.patches.spoof;
import app.revanced.integrations.settings.SettingsEnum;
public class SpoofDeviceDimensionsPatch {
private static final boolean SPOOF = SettingsEnum.SPOOF_DEVICE_DIMENSIONS.getBoolean();
public static int getMinHeightOrWidth(int minHeightOrWidth) {
return SPOOF ? 64 : minHeightOrWidth;
}
public static int getMaxHeightOrWidth(int maxHeightOrWidth) {
return SPOOF ? 4096 : maxHeightOrWidth;
}
}

View File

@ -3,6 +3,7 @@ package app.revanced.integrations.patches.spoof.requests;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.requests.Route;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import org.json.JSONException;
import org.json.JSONObject;
@ -75,7 +76,12 @@ final class PlayerRoutes {
/** @noinspection SameParameterValue*/
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException {
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip");
connection.setRequestProperty(
"User-Agent", "com.google.android.youtube/" +
ReVancedUtils.getVersionName() +
" (Linux; U; Android 12; GB) gzip"
);
connection.setRequestProperty("X-Goog-Api-Format-Version", "2");
connection.setRequestProperty("Content-Type", "application/json");

View File

@ -1,9 +1,10 @@
package app.revanced.integrations.patches.theme;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.ThemeHelper;
public class ThemeLithoComponentsPatch {
public class ThemePatch {
// color constants used in relation with litho components
private static final int[] WHITE_VALUES = {
-1, // comments chip background
@ -40,6 +41,10 @@ public class ThemeLithoComponentsPatch {
return originalValue;
}
public static boolean gradientLoadingScreenEnabled() {
return SettingsEnum.GRADIENT_LOADING_SCREEN.getBoolean();
}
private static int getBlackColor() {
if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1");
return blackColor;

View File

@ -1,5 +1,6 @@
package app.revanced.integrations.requests;
import app.revanced.integrations.utils.ReVancedUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -23,7 +24,7 @@ public class Requester {
String url = apiUrl + route.getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(route.getMethod().name());
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced");
connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + "; ReVanced/" + ReVancedUtils.getVersionName());
return connection;
}

View File

@ -10,10 +10,7 @@ import app.revanced.integrations.utils.StringRef;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.*;
import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE;
@ -123,6 +120,7 @@ public enum SettingsEnum {
HIDE_SUBSCRIPTIONS_BUTTON("revanced_hide_subscriptions_button", BOOLEAN, FALSE, true),
HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE),
HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE),
HIDE_VIDEO_CHANNEL_WATERMARK("revanced_hide_channel_watermark", BOOLEAN, TRUE),
PLAYER_POPUP_PANELS("revanced_hide_player_popup_panels", BOOLEAN, FALSE),
SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true),
SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"),
@ -130,6 +128,7 @@ public enum SettingsEnum {
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"),
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
GRADIENT_LOADING_SCREEN("revanced_gradient_loading_screen", BOOLEAN, FALSE),
SEEKBAR_CUSTOM_COLOR("revanced_seekbar_custom_color", BOOLEAN, TRUE, true),
SEEKBAR_CUSTOM_COLOR_VALUE("revanced_seekbar_custom_color_value", STRING, "#FF0000", true, parents(SEEKBAR_CUSTOM_COLOR)),
HIDE_FILTER_BAR_FEED_IN_FEED("revanced_hide_filter_bar_feed_in_feed", BOOLEAN, FALSE, true),
@ -175,7 +174,11 @@ public enum SettingsEnum {
"revanced_spoof_signature_verification_enabled_user_dialog_message"),
SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false,
parents(SPOOF_SIGNATURE)),
SPOOF_DEVICE_DIMENSIONS("revanced_spoof_device_dimensions", BOOLEAN, FALSE, true),
BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE),
ANNOUNCEMENTS("revanced_announcements", BOOLEAN, TRUE),
ANNOUNCEMENT_CONSUMER("revanced_announcement_consumer", STRING, ""),
ANNOUNCEMENT_LAST_HASH("revanced_announcement_last_hash", STRING, ""),
// Swipe controls
SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE),
@ -374,6 +377,8 @@ public enum SettingsEnum {
// region Migration
migrateOldSettingToNew(HIDE_VIDEO_WATERMARK, HIDE_VIDEO_CHANNEL_WATERMARK);
// Do _not_ delete this SB private user id migration property until sometime in 2024.
// This is the only setting that cannot be reconfigured if lost,
// and more time should be given for users who rarely upgrade.
@ -550,6 +555,7 @@ public enum SettingsEnum {
private boolean includeWithImportExport() {
switch (this) {
case RYD_USER_ID: // Not useful to export, no reason to include it.
case ANNOUNCEMENT_CONSUMER: // Not useful to export, no reason to include it.
case SB_LAST_VIP_CHECK:
case SB_HIDE_EXPORT_WARNING:
case SB_SEEN_GUIDELINES:

View File

@ -8,9 +8,17 @@ import java.util.Objects;
public final class ByteTrieSearch extends TrieSearch<byte[]> {
private static final class ByteTrieNode extends TrieNode<byte[]> {
TrieNode<byte[]> createNode() {
return new ByteTrieNode();
ByteTrieNode() {
super();
}
ByteTrieNode(char nodeCharacterValue) {
super(nodeCharacterValue);
}
@Override
TrieNode<byte[]> createNode(char nodeCharacterValue) {
return new ByteTrieNode(nodeCharacterValue);
}
@Override
char getCharValue(byte[] text, int index) {
return (char) text[index];
}

View File

@ -2,8 +2,11 @@ package app.revanced.integrations.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
@ -25,9 +28,36 @@ public class ReVancedUtils {
@SuppressLint("StaticFieldLeak")
public static Context context;
private static String versionName;
private ReVancedUtils() {
} // utility class
public static String getVersionName() {
if (versionName != null) return versionName;
PackageInfo packageInfo;
try {
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
packageInfo = context.getPackageManager().getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
);
else
packageInfo = context.getPackageManager().getPackageInfo(
packageName,
0
);
} catch (PackageManager.NameNotFoundException e) {
LogHelper.printException(() -> "Failed to get package info", e);
return null;
}
return versionName = packageInfo.versionName;
}
/**
* Hide a view by setting its layout height and width to 1dp.
*

View File

@ -11,9 +11,17 @@ import java.util.Objects;
public final class StringTrieSearch extends TrieSearch<String> {
private static final class StringTrieNode extends TrieNode<String> {
TrieNode<String> createNode() {
return new StringTrieNode();
StringTrieNode() {
super();
}
StringTrieNode(char nodeCharacterValue) {
super(nodeCharacterValue);
}
@Override
TrieNode<String> createNode(char nodeValue) {
return new StringTrieNode(nodeValue);
}
@Override
char getCharValue(String text, int index) {
return text.charAt(index);
}

View File

@ -71,15 +71,31 @@ public abstract class TrieSearch<T> {
}
static abstract class TrieNode<T> {
/**
* Dummy value used for root node. Value can be anything as it's never referenced.
*/
private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character.
// Support only ASCII letters/numbers/symbols and filter out all control characters.
private static final char MIN_VALID_CHAR = 32; // Space character.
private static final char MAX_VALID_CHAR = 126; // 127 = delete character.
private static final int NUMBER_OF_CHILDREN = MAX_VALID_CHAR - MIN_VALID_CHAR + 1;
/**
* How much to expand the children array when resizing.
*/
private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2;
private static final int CHILDREN_ARRAY_MAX_SIZE = MAX_VALID_CHAR - MIN_VALID_CHAR + 1;
private static boolean isInvalidRange(char character) {
return character < MIN_VALID_CHAR || character > MAX_VALID_CHAR;
}
/**
* Character this node represents.
* This field is ignored for the root node (which does not represent any character).
*/
private final char nodeValue;
/**
* A compressed graph path that represents the remaining pattern characters of a single child node.
*
@ -91,6 +107,24 @@ public abstract class TrieSearch<T> {
/**
* All child nodes. Only present if no compressed leaf exist.
*
* Array is dynamically increased in size as needed,
* and uses perfect hashing for the elements it contains.
*
* So if the array contains a given character,
* the character will always map to the node with index: (character % arraySize).
*
* Elements not contained can collide with elements the array does contain,
* so must compare the nodes character value.
*
* Alternatively this array could be a sorted and densely packed array,
* and lookup is done using binary search.
* That would save a small amount of memory because there's no null children entries,
* but would give a worst case search of O(nlog(m)) where n is the number of
* characters in the searched text and m is the maximum size of the sorted character arrays.
* Using a hash table array always gives O(n) search time.
* The memory usage here is very small (all Litho filters use ~10KB of memory),
* so the more performant hash implementation is chosen.
*/
@Nullable
private TrieNode<T>[] children;
@ -101,6 +135,13 @@ public abstract class TrieSearch<T> {
@Nullable
private List<TriePatternMatchedCallback<T>> endOfPatternCallback;
TrieNode() {
this.nodeValue = ROOT_NODE_CHARACTER_VALUE;
}
TrieNode(char nodeCharacterValue) {
this.nodeValue = nodeCharacterValue;
}
/**
* @param pattern Pattern to add.
* @param patternLength Length of the pattern.
@ -121,7 +162,7 @@ public abstract class TrieSearch<T> {
// Recursively call back into this method and push the existing leaf down 1 level.
if (children != null) throw new IllegalStateException();
//noinspection unchecked
children = new TrieNode[NUMBER_OF_CHILDREN];
children = new TrieNode[1];
TrieCompressedPath<T> temp = leaf;
leaf = null;
addPattern(temp.pattern, temp.patternLength, temp.patternStartIndex, temp.callback);
@ -130,19 +171,65 @@ public abstract class TrieSearch<T> {
leaf = new TrieCompressedPath<>(pattern, patternLength, patternIndex, callback);
return;
}
char character = getCharValue(pattern, patternIndex);
final char character = getCharValue(pattern, patternIndex);
if (isInvalidRange(character)) {
throw new IllegalArgumentException("invalid character at index " + patternIndex + ": " + pattern);
}
character -= MIN_VALID_CHAR; // Adjust to the array range.
TrieNode<T> child = children[character];
final int arrayIndex = hashIndexForTableSize(children.length, character);
TrieNode<T> child = children[arrayIndex];
if (child == null) {
child = createNode();
children[character] = child;
child = createNode(character);
children[arrayIndex] = child;
} else if (child.nodeValue != character) {
// Hash collision. Resize the table until perfect hashing is found.
child = createNode(character);
expandChildArray(child);
}
child.addPattern(pattern, patternLength, patternIndex + 1, callback);
}
/**
* Resizes the children table until all nodes hash to exactly one array index.
* Worse case, this will resize the array to {@link #CHILDREN_ARRAY_MAX_SIZE} elements.
*/
private void expandChildArray(TrieNode<T> child) {
int replacementArraySize = Objects.requireNonNull(children).length;
while (true) {
replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT;
//noinspection unchecked
TrieNode<T>[] replacement = new TrieNode[replacementArraySize];
addNodeToArray(replacement, child);
boolean collision = false;
for (TrieNode<T> existingChild : children) {
if (existingChild != null) {
if (!addNodeToArray(replacement, existingChild)) {
collision = true;
break;
}
}
}
if (collision) {
if (replacementArraySize > CHILDREN_ARRAY_MAX_SIZE) throw new IllegalStateException();
continue;
}
children = replacement;
return;
}
}
private static <T> boolean addNodeToArray(TrieNode<T>[] array, TrieNode<T> childToAdd) {
final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue);
if (array[insertIndex] != null ) {
return false; // Collision.
}
array[insertIndex] = childToAdd;
return true;
}
private static int hashIndexForTableSize(int arraySize, char nodeValue) {
return (nodeValue - MIN_VALID_CHAR) % arraySize;
}
/**
* @param searchText Text to search for patterns in.
* @param searchTextLength Length of the search text.
@ -170,18 +257,17 @@ public abstract class TrieSearch<T> {
if (children == null) {
return false; // Reached a graph end point and there's no further patterns to search.
}
if (searchTextIndex == searchTextLength) {
return false; // Reached end of the search text and found no matches.
}
char character = getCharValue(searchText, searchTextIndex);
final char character = getCharValue(searchText, searchTextIndex);
if (isInvalidRange(character)) {
return false; // Not an ASCII letter/number/symbol.
}
character -= MIN_VALID_CHAR; // Adjust to the array range.
TrieNode<T> child = children[character];
if (child == null) {
final int arrayIndex = hashIndexForTableSize(children.length, character);
TrieNode<T> child = children[arrayIndex];
if (child == null || child.nodeValue != character) {
return false;
}
return child.matches(searchText, searchTextLength, searchTextIndex + 1,
@ -194,7 +280,7 @@ public abstract class TrieSearch<T> {
* @return Estimated number of memory pointers used, starting from this node and including all children.
*/
private int estimatedNumberOfPointersUsed() {
int numberOfPointers = 3; // Number of fields in this class.
int numberOfPointers = 4; // Number of fields in this class.
if (leaf != null) {
numberOfPointers += 4; // Number of fields in leaf node.
}
@ -202,7 +288,7 @@ public abstract class TrieSearch<T> {
numberOfPointers += endOfPatternCallback.size();
}
if (children != null) {
numberOfPointers += NUMBER_OF_CHILDREN;
numberOfPointers += children.length;
for (TrieNode<T> child : children) {
if (child != null) {
numberOfPointers += child.estimatedNumberOfPointersUsed();
@ -212,7 +298,7 @@ public abstract class TrieSearch<T> {
return numberOfPointers;
}
abstract TrieNode<T> createNode();
abstract TrieNode<T> createNode(char nodeValue);
abstract char getCharValue(T text, int index);
}

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
android.useAndroidX = true
version = 0.119.2
version = 0.120.0-dev.6