From f8184905bd9601bf63e30963ff337d24f2599794 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 29 Mar 2023 19:36:16 +0200 Subject: [PATCH 01/30] feat(youtube/general-ads): hide new type of ad --- .../java/app/revanced/integrations/patches/GeneralAdsPatch.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index 985b7b33..ac469410 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -49,6 +49,7 @@ public final class GeneralAdsPatch extends Filter { "_buttoned_layout", "full_width_square_image_layout", "_ad_with", + "video_display_button_group_layout", "landscape_image_wide_button_layout" ); var generalAds = new BlockRule( From 6528d444b49759ee1137c9b0eb8e1079fb4cc97c Mon Sep 17 00:00:00 2001 From: johnconner122 <107796137+johnconner122@users.noreply.github.com> Date: Sat, 1 Apr 2023 14:09:16 +0500 Subject: [PATCH 02/30] fix(youtube/general-ads): hide new types of ads (#339) --- .../app/revanced/integrations/patches/GeneralAdsPatch.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index ac469410..eb5a988c 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -62,7 +62,9 @@ public final class GeneralAdsPatch extends Filter { "hero_promo_image", "statement_banner", "carousel_footered_layout", - "text_image_button_layout" + "text_image_button_layout", + "primetime_promo", + "feature_grid_interstitial" ); var movieAds = new BlockRule( SettingsEnum.ADREMOVER_MOVIE_REMOVAL, From e3529cfcec3b15e2187a1dd91a9f60295bd054a9 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 2 Apr 2023 18:10:01 +0400 Subject: [PATCH 03/30] refactor(youtube/sponsorblock): improve various implementations (#308) Co-authored-by: oSumAtrIX --- .../patches/CopyVideoUrlPatch.java | 8 +- .../integrations/patches/LithoThemePatch.java | 23 +- .../integrations/patches/MicroGSupport.java | 10 +- .../SpoofSignatureVerificationPatch.java | 11 +- .../patches/VideoInformation.java | 111 ++- .../downloads/views/DownloadOptions.kt | 4 - .../quality/RememberVideoQualityPatch.java | 43 +- .../speed/RememberPlaybackSpeedPatch.java | 10 +- .../ReturnYouTubeDislike.java | 7 +- .../requests/ReturnYouTubeDislikeApi.java | 20 +- .../integrations/settings/SettingsEnum.java | 82 +- .../settingsmenu/ReVancedSettingActivity.java | 15 +- .../ReVancedSettingsFragment.java | 19 +- .../ReturnYouTubeDislikeSettingsFragment.java | 4 +- .../SponsorBlockSettingsFragment.java | 758 +++++++++------- .../PlayerControlsVisibilityObserver.kt | 4 +- .../integrations/shared/PlayerType.kt | 5 +- .../sponsorblock/InjectedPlugin.java | 97 -- .../sponsorblock/NewSegmentHelperLayout.java | 28 - .../sponsorblock/PlayerController.java | 431 --------- .../SegmentPlaybackController.java | 595 +++++++++++++ .../sponsorblock/ShieldButton.java | 120 --- .../sponsorblock/SkipSegmentView.java | 46 - .../sponsorblock/SponsorBlockSettings.java | 329 ++++--- .../sponsorblock/SponsorBlockUtils.java | 828 +++++++----------- .../sponsorblock/SwipeHelper.java | 90 -- .../sponsorblock/VotingButton.java | 118 --- .../objects/CategoryBehaviour.java | 90 ++ .../objects/EditTextListPreference.java | 124 --- .../sponsorblock/objects/SegmentCategory.java | 305 +++++++ .../SegmentCategoryListPreference.java | 155 ++++ .../sponsorblock/objects/SponsorSegment.java | 131 ++- .../sponsorblock/objects/UserStats.java | 60 +- .../sponsorblock/player/ChannelModel.java | 29 - .../sponsorblock/player/PlayerType.java | 18 - .../player/ui/ButtonVisibility.java | 40 - .../player/ui/NewSegmentLayout.java | 151 ---- .../player/ui/SkipSponsorButton.java | 124 --- .../sponsorblock/player/ui/SlimButton.java | 67 -- .../player/ui/SponsorBlockView.java | 175 ---- .../player/ui/SponsorBlockVoting.java | 26 - .../sponsorblock/player/ui/Visibility.java | 8 - .../sponsorblock/requests/SBRequester.java | 289 +++--- .../sponsorblock/requests/SBRoutes.java | 8 +- .../ui/CreateSegmentButtonController.java | 124 +++ .../sponsorblock/ui/NewSegmentLayout.java | 124 +++ .../sponsorblock/ui/SkipSponsorButton.java | 103 +++ .../ui/SponsorBlockViewController.java | 228 +++++ .../ui/VotingButtonController.java | 125 +++ .../SwipeControlsHostActivity.kt | 6 +- .../controller/SwipeZonesController.kt | 2 +- .../views/SwipeControlsOverlayLayout.kt | 2 +- .../app/revanced/integrations/utils/Event.kt | 8 + .../integrations/utils/LogHelper.java | 9 +- .../integrations/utils/ReVancedUtils.java | 194 ++-- .../integrations/utils/SharedPrefHelper.java | 6 +- .../{sponsorblock => utils}/StringRef.java | 35 +- .../integrations/utils/ThemeHelper.java | 13 +- .../videoplayer/BottomControlButton.java | 25 +- .../videoplayer/DownloadButton.java | 6 +- 60 files changed, 3432 insertions(+), 3194 deletions(-) delete mode 100644 app/src/main/java/app/revanced/integrations/patches/downloads/views/DownloadOptions.kt delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/InjectedPlugin.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/NewSegmentHelperLayout.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/PlayerController.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/ShieldButton.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/SkipSegmentView.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/SwipeHelper.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/VotingButton.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/objects/EditTextListPreference.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ChannelModel.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/PlayerType.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/ButtonVisibility.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/NewSegmentLayout.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SkipSponsorButton.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SlimButton.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockView.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockVoting.java delete mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/Visibility.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java create mode 100644 app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java rename app/src/main/java/app/revanced/integrations/{sponsorblock => utils}/StringRef.java (79%) diff --git a/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java b/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java index f59134ed..a38ac8fd 100644 --- a/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/CopyVideoUrlPatch.java @@ -1,9 +1,7 @@ package app.revanced.integrations.patches; -import android.content.Context; -import android.widget.Toast; +import static app.revanced.integrations.utils.StringRef.str; -import app.revanced.integrations.sponsorblock.StringRef; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; @@ -16,10 +14,8 @@ public class CopyVideoUrlPatch { url += String.format("?t=%s", seconds); } - Context context = ReVancedUtils.getContext(); - ReVancedUtils.setClipboard(url); - if (context != null) Toast.makeText(context, StringRef.str("share_copy_url_success"), Toast.LENGTH_SHORT).show(); + ReVancedUtils.showToastShort(str("share_copy_url_success")); } catch (Exception e) { LogHelper.printException(() -> "Failed to generate video url", e); } diff --git a/app/src/main/java/app/revanced/integrations/patches/LithoThemePatch.java b/app/src/main/java/app/revanced/integrations/patches/LithoThemePatch.java index cc2a131b..f544970b 100644 --- a/app/src/main/java/app/revanced/integrations/patches/LithoThemePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/LithoThemePatch.java @@ -1,9 +1,6 @@ package app.revanced.integrations.patches; -import static app.revanced.integrations.utils.ReVancedUtils.getContext; - -import android.content.Context; - +import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.ThemeHelper; public class LithoThemePatch { @@ -44,29 +41,15 @@ public class LithoThemePatch { } private static int getBlackColor() { - if (blackColor == 0) blackColor = getColor("yt_black1"); + if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1"); return blackColor; } private static int getWhiteColor() { - if (whiteColor == 0) whiteColor = getColor("yt_white1"); + if (whiteColor == 0) whiteColor = ReVancedUtils.getResourceColor("yt_white1"); return whiteColor; } - /** - * Determines the color for a color resource. - * - * @param name The color resource name. - * @return The value of the color. - */ - private static int getColor(String name) { - Context context = getContext(); - - return context != null ? context.getColor(context.getResources() - .getIdentifier(name, "color", context.getPackageName()) - ) : 0; - } - private static boolean anyEquals(int value, int... of) { for (int v : of) if (value == v) return true; return false; diff --git a/app/src/main/java/app/revanced/integrations/patches/MicroGSupport.java b/app/src/main/java/app/revanced/integrations/patches/MicroGSupport.java index 76a9c9a5..b9f9d297 100644 --- a/app/src/main/java/app/revanced/integrations/patches/MicroGSupport.java +++ b/app/src/main/java/app/revanced/integrations/patches/MicroGSupport.java @@ -1,16 +1,16 @@ package app.revanced.integrations.patches; +import static app.revanced.integrations.utils.StringRef.str; + import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; -import android.widget.Toast; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; import java.util.Objects; -import static app.revanced.integrations.sponsorblock.StringRef.str; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; public class MicroGSupport { private static final String MICROG_VENDOR = "com.mgoogle"; @@ -20,7 +20,7 @@ public class MicroGSupport { private static final Uri VANCED_MICROG_PROVIDER = Uri.parse("content://" + MICROG_VENDOR + ".android.gsf.gservices/prefix"); private static void startIntent(Context context, String uriString, String message) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + ReVancedUtils.showToastLong(message); var intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java b/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java index d05eeafc..3b46d2be 100644 --- a/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/SpoofSignatureVerificationPatch.java @@ -1,7 +1,5 @@ package app.revanced.integrations.patches; -import android.widget.Toast; - import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.utils.LogHelper; @@ -83,13 +81,8 @@ public class SpoofSignatureVerificationPatch { } SettingsEnum.SIGNATURE_SPOOFING.saveValue(true); - ReVancedUtils.runOnMainThread(() -> { - Toast.makeText( - ReVancedUtils.getContext(), - "Spoofing app signature to prevent playback issues", Toast.LENGTH_LONG - ).show(); - // it would be great if the video could be forcefully reloaded, but currently there is no code to do this - }); + ReVancedUtils.showToastLong("Spoofing app signature to prevent playback issues"); + // it would be great if the video could be forcefully reloaded, but currently there is no code to do this } catch (Exception ex) { LogHelper.printException(() -> "onResponse failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java index 4c02a3d3..21299163 100644 --- a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java @@ -1,12 +1,12 @@ package app.revanced.integrations.patches; -import android.os.Handler; -import android.os.Looper; +import androidx.annotation.NonNull; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; /** * Hooking class for the current playing video. @@ -17,19 +17,20 @@ public final class VideoInformation { private static WeakReference playerController; private static Method seekMethod; + @NonNull private static String videoId = ""; - private static long videoLength = 1; - private static long videoTime = -1; - + private static long videoLength = 0; + private static volatile long videoTime = -1; // must be volatile. Value is set off main thread from high precision patch hook /** - * Hook into PlayerController.onCreate() method. + * Injection point. + * Sets a reference to the YouTube playback controller. * * @param thisRef Reference to the player controller object. */ public static void playerController_onCreateHook(final Object thisRef) { playerController = new WeakReference<>(thisRef); - videoLength = 1; + videoLength = 0; videoTime = -1; try { @@ -41,81 +42,115 @@ public final class VideoInformation { } /** - * Set the video id. + * Injection point. * - * @param videoId The id of the video. + * @param newlyLoadedVideoId id of the current video */ - public static void setVideoId(String videoId) { - LogHelper.printDebug(() -> "Setting current video id to: " + videoId); - - VideoInformation.videoId = videoId; + public static void setVideoId(@NonNull String newlyLoadedVideoId) { + if (!videoId.equals(newlyLoadedVideoId)) { + LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId); + videoId = newlyLoadedVideoId; + } } /** - * Set the video length. + * Injection point. * * @param length The length of the video in milliseconds. */ public static void setVideoLength(final long length) { - LogHelper.printDebug(() -> "Setting current video length to " + length); - videoLength = length; + if (videoLength != length) { + LogHelper.printDebug(() -> "Current video length: " + length); + videoLength = length; + } } /** - * Set the video time. + * Injection point. + * Called off the main thread approximately every 50ms to 100ms * - * @param time The time of the video in milliseconds. + * @param currentPlaybackTime The current playback time of the video in milliseconds. */ - public static void setVideoTime(final long time) { - LogHelper.printDebug(() -> "Current video time " + time); - videoTime = time; + public static void setVideoTimeHighPrecision(final long currentPlaybackTime) { + videoTime = currentPlaybackTime; } /** * Seek on the current video. + * Does not function for playback of Shorts or Stories. + * + * Caution: If called from a videoTimeHook() callback, + * this will cause a recursive call into the same videoTimeHook() callback. * * @param millisecond The millisecond to seek the video to. + * @return if the seek was successful */ - public static void seekTo(final long millisecond) { - new Handler(Looper.getMainLooper()).post(() -> { - if (seekMethod == null) { - LogHelper.printDebug(() -> "seekMethod was null"); - return; - } + public static boolean seekTo(final long millisecond) { + ReVancedUtils.verifyOnMainThread(); + if (seekMethod == null) { + LogHelper.printException(() -> "seekMethod was null"); + return false; + } - try { - LogHelper.printDebug(() -> "Seeking to " + millisecond); - seekMethod.invoke(playerController.get(), millisecond); - } catch (Exception ex) { - LogHelper.printException(() -> "Failed to seek", ex); - } - }); + try { + LogHelper.printDebug(() -> "Seeking to " + millisecond); + return (Boolean) seekMethod.invoke(playerController.get(), millisecond); + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to seek", ex); + return false; + } + } + + public static boolean seekToRelative(long millisecondsRelative) { + return seekTo(videoTime + millisecondsRelative); } /** - * Get the id of the current video playing. + * Id of the current video playing. Includes Shorts and YouTube Stories. * * @return The id of the video. Empty string if not set yet. */ + @NonNull public static String getCurrentVideoId() { return videoId; } /** - * Get the length of the current video playing. + * Length of the current video playing. + * Includes Shorts playback. * - * @return The length of the video in milliseconds. 1 if not set yet. + * @return The length of the video in milliseconds. + * If the video is not yet loaded, or if the video is playing in the background with no video visible, + * then this returns zero. */ public static long getCurrentVideoLength() { return videoLength; } /** - * Get the time of the current video playing. + * Playback time of the current video playing. + * Value can lag up to approximately 100ms behind the actual current video playback time. + * + * Note: Code inside a videoTimeHook patch callback + * should use the callback video time and avoid using this method + * (in situations of recursive hook callbacks, the value returned here may be outdated). + * + * Includes Shorts playback. * * @return The time of the video in milliseconds. -1 if not set yet. */ public static long getVideoTime() { return videoTime; } + + /** + * @return If the playback is at the end of the video. + * + * If video is playing in the background with no video visible, + * this always returns false (even if the video is actually at the end) + */ + public static boolean isAtEndOfVideo() { + return videoTime > 0 && videoLength > 0 && videoTime >= videoLength; + } + } diff --git a/app/src/main/java/app/revanced/integrations/patches/downloads/views/DownloadOptions.kt b/app/src/main/java/app/revanced/integrations/patches/downloads/views/DownloadOptions.kt deleted file mode 100644 index 2be9ed19..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/downloads/views/DownloadOptions.kt +++ /dev/null @@ -1,4 +0,0 @@ -package app.revanced.integrations.patches.downloads.views - -class DownloadOptions { -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java index d5c79a7d..a9fd837e 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java @@ -1,18 +1,18 @@ package app.revanced.integrations.patches.playback.quality; import android.content.Context; -import android.net.ConnectivityManager; -import android.widget.Toast; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.utils.ReVancedUtils.NetworkType; +import app.revanced.integrations.utils.SharedPrefHelper; + public class RememberVideoQualityPatch { public static int selectedQuality1 = -2; @@ -22,12 +22,10 @@ public class RememberVideoQualityPatch { public static void changeDefaultQuality(int defaultQuality) { Context context = ReVancedUtils.getContext(); - var networkType = getNetworType(context); + var networkType = ReVancedUtils.getNetworkType(); if (networkType == NetworkType.NONE) { - String message = "No internet connection."; - LogHelper.printDebug(() -> message); - Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + ReVancedUtils.showToastShort("No internet connection."); } else { var preferenceKey = "wifi_quality"; var networkTypeMessage = "WIFI"; @@ -38,8 +36,7 @@ public class RememberVideoQualityPatch { } SharedPrefHelper.saveString(SharedPrefHelper.SharedPrefNames.REVANCED_PREFS, preferenceKey, defaultQuality + ""); - String message = "Changing default " + networkTypeMessage + " quality to: " + defaultQuality; - Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + ReVancedUtils.showToastShort("Changing default " + networkTypeMessage + " quality to: " + defaultQuality); } userChangedQuality = false; @@ -89,7 +86,7 @@ public class RememberVideoQualityPatch { LogHelper.printException(() -> "Context is null or settings not initialized, returning quality: " + qualityToLog); return quality; } - var networkType = getNetworType(context); + var networkType = ReVancedUtils.getNetworkType(); if (networkType == NetworkType.NONE) { LogHelper.printDebug(() -> "No Internet connection!"); return quality; @@ -140,24 +137,4 @@ public class RememberVideoQualityPatch { public static void newVideoStarted(String videoId) { newVideo = true; } - - private static NetworkType getNetworType(Context context) { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - var networkInfo = cm.getActiveNetworkInfo(); - - if (networkInfo == null || !networkInfo.isConnected()) { - return NetworkType.NONE; - } else { - var type = networkInfo.getType(); - - return type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_BLUETOOTH ? NetworkType.MOBILE : NetworkType.OTHER; - } - } - - enum NetworkType { - MOBILE, - OTHER, - NONE - } - } diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java index c1d4a8ad..8b4b2114 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -1,8 +1,8 @@ package app.revanced.integrations.patches.playback.speed; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; @@ -19,10 +19,6 @@ public final class RememberPlaybackSpeedPatch { @Nullable private static String currentVideoId; - private static void showToast(final String message) { - Toast.makeText(ReVancedUtils.getContext(), message, Toast.LENGTH_LONG).show(); - } - private static float getLastRememberedPlaybackSpeed() { return SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat(); } @@ -62,11 +58,11 @@ public final class RememberPlaybackSpeedPatch { if (rememberLastSelectedPlaybackSpeed()) { rememberPlaybackSpeed(); - showToast("Remembering playback speed: " + playbackSpeed + "x"); + ReVancedUtils.showToastLong("Remembering playback speed: " + playbackSpeed + "x"); } else { if (getLastRememberedPlaybackSpeed() == DEFAULT_PLAYBACK_SPEED) return; - showToast("Applying playback speed: " + playbackSpeed + "x"); + ReVancedUtils.showToastLong("Applying playback speed: " + playbackSpeed + "x"); } } diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index f66180eb..26b0b9aa 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -1,6 +1,6 @@ package app.revanced.integrations.returnyoutubedislike; -import static app.revanced.integrations.sponsorblock.StringRef.str; +import static app.revanced.integrations.utils.StringRef.str; import android.graphics.Canvas; import android.graphics.Paint; @@ -177,6 +177,11 @@ public class ReturnYouTubeDislike { if (videoId.equals(currentVideoId)) { return; // already loaded } + if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id + LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId); + setCurrentVideoId(null); + return; + } LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType); setCurrentVideoId(videoId); // no need to wrap the call in a try/catch, diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index e7c7145e..ae154cfe 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -1,12 +1,12 @@ package app.revanced.integrations.returnyoutubedislike.requests; +import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; +import static app.revanced.integrations.utils.StringRef.str; + import android.util.Base64; -import android.widget.Toast; + import androidx.annotation.Nullable; -import app.revanced.integrations.requests.Requester; -import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; + import org.json.JSONException; import org.json.JSONObject; @@ -20,8 +20,10 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Objects; -import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; -import static app.revanced.integrations.sponsorblock.StringRef.str; +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; public class ReturnYouTubeDislikeApi { /** @@ -192,9 +194,7 @@ public class ReturnYouTubeDislikeApi { numberOfRateLimitRequestsEncountered++; LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " + RATE_LIMIT_BACKOFF_SECONDS + " seconds"); - ReVancedUtils.runOnMainThread(() -> { // must show toasts on main thread - Toast.makeText(ReVancedUtils.getContext(), str("revanced_ryd_failure_client_rate_limit_requested"), Toast.LENGTH_LONG).show(); - }); + ReVancedUtils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); return true; } return false; diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index c48ee151..1f34db68 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -1,6 +1,5 @@ package app.revanced.integrations.settings; -import android.content.Context; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.SharedPrefHelper; @@ -131,20 +130,20 @@ public enum SettingsEnum { // SponsorBlock settings SB_ENABLED("sb-enabled", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_SHOW_TOAST_WHEN_SKIP("show-toast", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_COUNT_SKIPS("count-skips", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), + SB_VOTING_ENABLED("sb-voting-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), + SB_CREATE_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), + SB_USE_COMPACT_SKIPBUTTON("sb-use-compact-skip-button", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), + SB_SHOW_TOAST_ON_SKIP("show-toast", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), + SB_TRACK_SKIP_COUNT("count-skips", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), SB_UUID("uuid", "", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING), SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", 150, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER), SB_MIN_DURATION("sb-min-duration", 0F, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.FLOAT), SB_SEEN_GUIDELINES("sb-seen-gl", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_VOTING_ENABLED("sb-voting-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_SKIPPED_SEGMENTS("sb-skipped-segments", 0, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER), - SB_SKIPPED_SEGMENTS_TIME("sb-skipped-segments-time", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG), + SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED("sb-skipped-segments", 0, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER), + SB_SKIPPED_SEGMENTS_TIME_SAVED("sb-skipped-segments-time", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG), SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), SB_IS_VIP("sb-is-vip", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), SB_LAST_VIP_CHECK("sb-last-vip-check", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG), - SB_SHOW_BROWSER_BUTTON("sb-browser-button", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), SB_API_URL("sb-api-host-url", "https://sponsor.ajay.app", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING); private final String path; @@ -178,39 +177,39 @@ public enum SettingsEnum { } static { - load(); + loadAllSettings(); } - private static void load() { - Context context = ReVancedUtils.getContext(); - if (context == null) { - LogHelper.printException(() -> "SettingsEnum.load() called before ReVancedUtils.init()"); + private static void loadAllSettings() { + if (ReVancedUtils.getContext() == null) { + LogHelper.printException(() -> "SettingsEnum loaded before ReVancedUtils context was set"); return; } for (SettingsEnum setting : values()) { - var path = setting.getPath(); - var defaultValue = setting.getDefaultValue(); - switch (setting.getReturnType()) { - case FLOAT: - defaultValue = SharedPrefHelper.getFloat(setting.sharedPref, path, (float) defaultValue); - break; - case LONG: - defaultValue = SharedPrefHelper.getLong(setting.sharedPref, path, (long) defaultValue); - break; - case BOOLEAN: - defaultValue = SharedPrefHelper.getBoolean(setting.sharedPref, path, (boolean) defaultValue); - break; - case INTEGER: - defaultValue = SharedPrefHelper.getInt(setting.sharedPref, path, (int) defaultValue); - break; - case STRING: - defaultValue = SharedPrefHelper.getString(setting.sharedPref, path, (String) defaultValue); - break; - default: - LogHelper.printException(() -> "Setting does not have a valid Type. Name is: " + setting.name()); - break; - } - setting.setValue(defaultValue); + setting.load(); + } + } + + private void load() { + switch (returnType) { + case FLOAT: + value = SharedPrefHelper.getFloat(sharedPref, path, (float) defaultValue); + break; + case LONG: + value = SharedPrefHelper.getLong(sharedPref, path, (long) defaultValue); + break; + case BOOLEAN: + value = SharedPrefHelper.getBoolean(sharedPref, path, (boolean) defaultValue); + break; + case INTEGER: + value = SharedPrefHelper.getInt(sharedPref, path, (int) defaultValue); + break; + case STRING: + value = SharedPrefHelper.getString(sharedPref, path, (String) defaultValue); + break; + default: + LogHelper.printException(() -> "Setting does not have a valid Type: " + name()); + break; } } @@ -227,14 +226,7 @@ public enum SettingsEnum { * Sets the value, and persistently saves it */ public void saveValue(Object newValue) { - Context context = ReVancedUtils.getContext(); - - if (context == null) { - LogHelper.printException(() -> "Context on SaveValue is null!"); - return; - } - - switch (getReturnType()) { + switch (returnType) { case FLOAT: SharedPrefHelper.saveFloat(sharedPref, path, (float) newValue); break; @@ -251,7 +243,7 @@ public enum SettingsEnum { SharedPrefHelper.saveString(sharedPref, path, (String) newValue); break; default: - LogHelper.printException(() -> "Setting does not have a valid Type. Name is: " + name()); + LogHelper.printException(() -> "Setting does not have a valid Type: " + name()); break; } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java index c511f1a6..898d21c9 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java @@ -1,6 +1,5 @@ package app.revanced.integrations.settingsmenu; -import android.content.Context; import android.preference.PreferenceFragment; import android.view.View; import android.view.ViewGroup; @@ -24,11 +23,11 @@ public class ReVancedSettingActivity { final var theme = ThemeHelper.isDarkTheme() ? darkTheme : whiteTheme; LogHelper.printDebug(() -> "Using theme: " + theme); - base.setTheme(getIdentifier(theme, "style")); + base.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style")); } public static void initializeSettings(LicenseActivity base) { - base.setContentView(getIdentifier("revanced_settings_with_toolbar", "layout")); + base.setContentView(ReVancedUtils.getResourceIdentifier("revanced_settings_with_toolbar", "layout")); PreferenceFragment preferenceFragment; String preferenceIdentifier; @@ -46,7 +45,7 @@ public class ReVancedSettingActivity { } try { - TextView toolbar = getTextView((ViewGroup) base.findViewById(getIdentifier("toolbar", "id"))); + TextView toolbar = getTextView((ViewGroup) base.findViewById(ReVancedUtils.getResourceIdentifier("toolbar", "id"))); if (toolbar == null) { // FIXME // https://github.com/revanced/revanced-patches/issues/1384 @@ -58,7 +57,7 @@ public class ReVancedSettingActivity { LogHelper.printException(() -> "Could not set Toolbar title", e); } - base.getFragmentManager().beginTransaction().replace(getIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit(); + base.getFragmentManager().beginTransaction().replace(ReVancedUtils.getResourceIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit(); } @@ -86,10 +85,4 @@ public class ReVancedSettingActivity { public static TextView getTextView(ViewGroup viewGroup) { return getView(TextView.class, viewGroup); } - - private static int getIdentifier(String name, String defType) { - Context appContext = ReVancedUtils.getContext(); - assert appContext != null; - return appContext.getResources().getIdentifier(name, defType, appContext.getPackageName()); - } } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index c7a3535f..308f348d 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -1,5 +1,7 @@ package app.revanced.integrations.settingsmenu; +import static app.revanced.integrations.utils.StringRef.str; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlarmManager; @@ -8,11 +10,9 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Resources; import android.os.Bundle; import android.os.Process; import android.preference.EditTextPreference; -import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; @@ -86,8 +86,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment { super.onCreate(bundle); getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.YOUTUBE.getName()); try { - int identifier = getResources().getIdentifier("revanced_prefs", "xml", getPackageName()); - addPreferencesFromResource(identifier); + addPreferencesFromResource(ReVancedUtils.getResourceIdentifier("revanced_prefs", "xml")); SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); this.settingsInitialized = sharedPreferences.getBoolean("revanced_initialized", false); @@ -161,17 +160,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment { } private void rebootDialog(final Activity activity) { - new AlertDialog.Builder(activity).setMessage(getStringByName(activity, "pref_refresh_config")).setPositiveButton(getStringByName(activity, "in_app_update_restart_button"), (dialog, id) -> reboot(activity, Shell_HomeActivity.class)).setNegativeButton(getStringByName(activity, "sign_in_cancel"), null).show(); - } - - private String getStringByName(Context context, String name) { - try { - Resources res = context.getResources(); - return res.getString(res.getIdentifier(name, "string", context.getPackageName())); - } catch (Throwable exception) { - LogHelper.printException(() -> "Resource not found.", exception); - return ""; - } + new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")).setPositiveButton(str("in_app_update_restart_button"), (dialog, id) -> reboot(activity, Shell_HomeActivity.class)).setNegativeButton(str("sign_in_cancel"), null).show(); } } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index 2e883a08..41d5eb21 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -1,6 +1,6 @@ package app.revanced.integrations.settingsmenu; -import static app.revanced.integrations.sponsorblock.StringRef.str; +import static app.revanced.integrations.utils.StringRef.str; import android.app.Activity; import android.content.Intent; @@ -102,7 +102,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { // About category PreferenceCategory aboutCategory = new PreferenceCategory(context); - aboutCategory.setTitle(str("about")); + aboutCategory.setTitle(str("revanced_ryd_about")); preferenceScreen.addPreference(aboutCategory); // ReturnYouTubeDislike Website diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java index de318631..f1c1bbff 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -1,206 +1,373 @@ package app.revanced.integrations.settingsmenu; -import static app.revanced.integrations.sponsorblock.StringRef.str; +import static android.text.Html.fromHtml; +import static app.revanced.integrations.utils.StringRef.str; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.SharedPreferences; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.preference.SwitchPreference; -import android.text.Editable; import android.text.Html; import android.text.InputType; -import android.util.Patterns; import android.widget.EditText; -import android.widget.Toast; -import java.lang.ref.WeakReference; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.text.DecimalFormat; -import java.util.ArrayList; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.sponsorblock.SegmentPlaybackController; import app.revanced.integrations.sponsorblock.SponsorBlockSettings; import app.revanced.integrations.sponsorblock.SponsorBlockUtils; -import app.revanced.integrations.utils.SharedPrefHelper; -import app.revanced.integrations.sponsorblock.objects.EditTextListPreference; +import app.revanced.integrations.sponsorblock.objects.SegmentCategory; +import app.revanced.integrations.sponsorblock.objects.SegmentCategoryListPreference; +import app.revanced.integrations.sponsorblock.objects.UserStats; import app.revanced.integrations.sponsorblock.requests.SBRequester; +import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.utils.SharedPrefHelper; -public class SponsorBlockSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final DecimalFormat FORMATTER = new DecimalFormat("#,###,###"); - public static final String SAVED_TEMPLATE = "%dh %.1f %s"; - private static final APIURLChangeListener API_URL_CHANGE_LISTENER = new APIURLChangeListener(); - private final ArrayList preferencesToDisableWhenSBDisabled = new ArrayList<>(); +@SuppressWarnings("deprecation") +public class SponsorBlockSettingsFragment extends PreferenceFragment { + + private SwitchPreference sbEnabled; + private SwitchPreference addNewSegment; + private SwitchPreference votingEnabled; + private SwitchPreference compactSkipButton; + private SwitchPreference showSkipToast; + private SwitchPreference trackSkips; + private SwitchPreference showTimeWithoutSegments; + + private EditTextPreference newSegmentStep; + private EditTextPreference minSegmentDuration; + private EditTextPreference privateUserId; + private EditTextPreference importExport; + private Preference apiUrl; + + private PreferenceCategory statsCategory; + private PreferenceCategory segmentCategory; + + private void updateUI() { + try { + final boolean enabled = SettingsEnum.SB_ENABLED.getBoolean(); + if (!enabled) { + SponsorBlockViewController.hideSkipButton(); + SponsorBlockViewController.hideNewSegmentLayout(); + SegmentPlaybackController.setCurrentVideoId(null); + } else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()) { + SponsorBlockViewController.hideNewSegmentLayout(); + } + // voting and add new segment buttons automatically shows/hides themselves + + sbEnabled.setChecked(enabled); + + addNewSegment.setChecked(SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()); + addNewSegment.setEnabled(enabled); + + votingEnabled.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean()); + votingEnabled.setEnabled(enabled); + + compactSkipButton.setChecked(SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean()); + compactSkipButton.setEnabled(enabled); + + showSkipToast.setChecked(SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean()); + showSkipToast.setEnabled(enabled); + + trackSkips.setChecked(SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()); + trackSkips.setEnabled(enabled); + + showTimeWithoutSegments.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()); + showTimeWithoutSegments.setEnabled(enabled); + + newSegmentStep.setText(String.valueOf(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt())); + newSegmentStep.setEnabled(enabled); + + minSegmentDuration.setText(String.valueOf(SettingsEnum.SB_MIN_DURATION.getFloat())); + minSegmentDuration.setEnabled(enabled); + + privateUserId.setText(SettingsEnum.SB_UUID.getString()); + privateUserId.setEnabled(enabled); + + apiUrl.setEnabled(enabled); + importExport.setEnabled(enabled); + segmentCategory.setEnabled(enabled); + statsCategory.setEnabled(enabled); + } catch (Exception ex) { + LogHelper.printException(() -> "update settings UI failure", ex); + } + } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK.getName()); + try { + PreferenceManager preferenceManager = getPreferenceManager(); + preferenceManager.setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK.getName()); - getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + Activity context = this.getActivity(); + PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(context); + setPreferenceScreen(preferenceScreen); - final Activity context = this.getActivity(); + SponsorBlockSettings.initialize(); - PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); - setPreferenceScreen(preferenceScreen); - - SponsorBlockSettings.update(getActivity()); - - { - SwitchPreference preference = new SwitchPreference(context); - preferenceScreen.addPreference(preference); - preference.setKey(SettingsEnum.SB_ENABLED.getPath()); - preference.setDefaultValue(SettingsEnum.SB_ENABLED.getDefaultValue()); - preference.setChecked(SettingsEnum.SB_ENABLED.getBoolean()); - preference.setTitle(str("enable_sb")); - preference.setSummary(str("enable_sb_sum")); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - final boolean value = (Boolean) newValue; - enableCategoriesIfNeeded(value); + sbEnabled = new SwitchPreference(context); + sbEnabled.setTitle(str("sb_enable_sb")); + sbEnabled.setSummary(str("sb_enable_sb_sum")); + preferenceScreen.addPreference(sbEnabled); + sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { SettingsEnum.SB_ENABLED.saveValue(newValue); + updateUI(); return true; }); - } - { - SwitchPreference preference = new SwitchPreference(context); - preferenceScreen.addPreference(preference); - preference.setKey(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getPath()); - preference.setDefaultValue(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getDefaultValue()); - preference.setChecked(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean()); - preference.setTitle(str("enable_segmadding")); - preference.setSummary(str("enable_segmadding_sum")); - preferencesToDisableWhenSBDisabled.add(preference); - preference.setOnPreferenceChangeListener((preference12, o) -> { - final boolean value = (Boolean) o; - if (value && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) { - new AlertDialog.Builder(preference12.getContext()) + addNewSegment = new SwitchPreference(context); + addNewSegment.setTitle(str("sb_enable_create_segment")); + addNewSegment.setSummaryOn(str("sb_enable_create_segment_sum_on")); + addNewSegment.setSummaryOff(str("sb_enable_create_segment_sum_off")); + preferenceScreen.addPreference(addNewSegment); + addNewSegment.setOnPreferenceChangeListener((preference1, o) -> { + Boolean newValue = (Boolean) o; + if (newValue && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) { + SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true); + new AlertDialog.Builder(preference1.getContext()) .setTitle(str("sb_guidelines_popup_title")) .setMessage(str("sb_guidelines_popup_content")) .setNegativeButton(str("sb_guidelines_popup_already_read"), null) .setPositiveButton(str("sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines()) .show(); } - SettingsEnum.SB_NEW_SEGMENT_ENABLED.saveValue(value); + SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue); + updateUI(); return true; }); - } - { - SwitchPreference preference = new SwitchPreference(context); - preferenceScreen.addPreference(preference); - preference.setTitle(str("enable_voting")); - preference.setSummary(str("enable_voting_sum")); - preference.setKey(SettingsEnum.SB_VOTING_ENABLED.getPath()); - preference.setDefaultValue(SettingsEnum.SB_VOTING_ENABLED.getDefaultValue()); - preference.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean()); - preferencesToDisableWhenSBDisabled.add(preference); - preference.setOnPreferenceChangeListener((preference12, o) -> { - final boolean value = (Boolean) o; - SettingsEnum.SB_VOTING_ENABLED.saveValue(value); + votingEnabled = new SwitchPreference(context); + votingEnabled.setTitle(str("sb_enable_voting")); + votingEnabled.setSummaryOn(str("sb_enable_voting_sum_on")); + votingEnabled.setSummaryOff(str("sb_enable_voting_sum_off")); + preferenceScreen.addPreference(votingEnabled); + votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_VOTING_ENABLED.saveValue(newValue); + updateUI(); return true; }); + + compactSkipButton = new SwitchPreference(context); + compactSkipButton.setTitle(str("sb_enable_compact_skip_button")); + compactSkipButton.setSummaryOn(str("sb_enable_compact_skip_button_sum_on")); + compactSkipButton.setSummaryOff(str("sb_enable_compact_skip_button_sum_off")); + preferenceScreen.addPreference(compactSkipButton); + compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.saveValue(newValue); + updateUI(); + return true; + }); + + addGeneralCategory(context, preferenceScreen); + + segmentCategory = new PreferenceCategory(context); + segmentCategory.setTitle(str("sb_diff_segments")); + preferenceScreen.addPreference(segmentCategory); + updateSegmentCategories(); + + statsCategory = new PreferenceCategory(context); + statsCategory.setTitle(str("sb_stats")); + preferenceScreen.addPreference(statsCategory); + fetchAndDisplayStats(); + + addAboutCategory(context, preferenceScreen); + + updateUI(); + } catch (Exception ex) { + LogHelper.printException(() -> "onCreate failure", ex); } - - addGeneralCategory(context, preferenceScreen); - addSegmentsCategory(context, preferenceScreen); - addStatsCategory(context, preferenceScreen); - addAboutCategory(context, preferenceScreen); - - enableCategoriesIfNeeded(SettingsEnum.SB_ENABLED.getBoolean()); } - private void openGuidelines() { - final Context context = getActivity(); - SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true); - - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines")); - context.startActivity(intent); - } - - private void enableCategoriesIfNeeded(boolean value) { - for (Preference preference : preferencesToDisableWhenSBDisabled) - preference.setEnabled(value); - } - - @Override - public void onDestroy() { - super.onDestroy(); - getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - } - - private void addSegmentsCategory(Context context, PreferenceScreen screen) { - PreferenceCategory category = new PreferenceCategory(context); + private void addGeneralCategory(final Context context, PreferenceScreen screen) { + final PreferenceCategory category = new PreferenceCategory(context); screen.addPreference(category); - preferencesToDisableWhenSBDisabled.add(category); - category.setTitle(str("diff_segments")); + category.setTitle(str("sb_general")); - SponsorBlockSettings.SegmentBehaviour[] segmentBehaviours = SponsorBlockSettings.SegmentBehaviour.values(); - String[] entries = new String[segmentBehaviours.length]; - String[] entryValues = new String[segmentBehaviours.length]; - for (int i = 0, segmentBehavioursLength = segmentBehaviours.length; i < segmentBehavioursLength; i++) { - SponsorBlockSettings.SegmentBehaviour behaviour = segmentBehaviours[i]; - entries[i] = behaviour.name.toString(); - entryValues[i] = behaviour.key; - } + Preference guidelinePreferences = new Preference(context); + guidelinePreferences.setTitle(str("sb_guidelines_preference_title")); + guidelinePreferences.setSummary(str("sb_guidelines_preference_sum")); + guidelinePreferences.setOnPreferenceClickListener(preference1 -> { + openGuidelines(); + return true; + }); + category.addPreference(guidelinePreferences); - SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted(); - for (SponsorBlockSettings.SegmentInfo segmentInfo : categories) { - EditTextListPreference preference = new EditTextListPreference(context); - preference.setTitle(segmentInfo.getTitleWithDot()); - preference.setSummary(segmentInfo.description.toString()); - preference.setKey(segmentInfo.key); - preference.setDefaultValue(segmentInfo.behaviour.key); - preference.setEntries(entries); - preference.setEntryValues(entryValues); + showSkipToast = new SwitchPreference(context); + showSkipToast.setTitle(str("sb_general_skiptoast")); + showSkipToast.setSummaryOn(str("sb_general_skiptoast_sum_on")); + showSkipToast.setSummaryOff(str("sb_general_skiptoast_sum_off")); + showSkipToast.setOnPreferenceClickListener(preference1 -> { + ReVancedUtils.showToastShort(str("sb_skipped_sponsor")); + return false; + }); + showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(newValue); + updateUI(); + return true; + }); + category.addPreference(showSkipToast); - category.addPreference(preference); - } - Preference colorPreference = new Preference(context); // TODO remove this after the next major update - screen.addPreference(colorPreference); - colorPreference.setTitle(str("color_change")); - colorPreference.setSummary(str("color_change_sum")); - colorPreference.setSelectable(false); - preferencesToDisableWhenSBDisabled.add(colorPreference); + trackSkips = new SwitchPreference(context); + trackSkips.setTitle(str("sb_general_skipcount")); + trackSkips.setSummaryOn(str("sb_general_skipcount_sum_on")); + trackSkips.setSummaryOff(str("sb_general_skipcount_sum_off")); + trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(newValue); + updateUI(); + return true; + }); + category.addPreference(trackSkips); + + + showTimeWithoutSegments = new SwitchPreference(context); + showTimeWithoutSegments.setTitle(str("sb_general_time_without")); + showTimeWithoutSegments.setSummaryOn(str("sb_general_time_without_sum_on")); + showTimeWithoutSegments.setSummaryOff(str("sb_general_time_without_sum_off")); + showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue); + updateUI(); + return true; + }); + category.addPreference(showTimeWithoutSegments); + + + newSegmentStep = new EditTextPreference(context); + newSegmentStep.setTitle(str("sb_general_adjusting")); + newSegmentStep.setSummary(str("sb_general_adjusting_sum")); + newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); + newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> { + final int newAdjustmentValue = Integer.parseInt(newValue.toString()); + if (newAdjustmentValue == 0) { + ReVancedUtils.showToastLong(str("sb_general_adjusting_invalid")); + return false; + } + SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue); + return true; + }); + category.addPreference(newSegmentStep); + + + minSegmentDuration = new EditTextPreference(context); + minSegmentDuration.setTitle(str("sb_general_min_duration")); + minSegmentDuration.setSummary(str("sb_general_min_duration_sum")); + minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> { + SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString())); + return true; + }); + category.addPreference(minSegmentDuration); + + + privateUserId = new EditTextPreference(context); + privateUserId.setTitle(str("sb_general_uuid")); + privateUserId.setSummary(str("sb_general_uuid_sum")); + privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> { + String newUUID = newValue.toString(); + if (!SponsorBlockSettings.isValidSBUserId(newUUID)) { + ReVancedUtils.showToastLong(str("sb_general_uuid_invalid")); + return false; + } + SettingsEnum.SB_UUID.saveValue(newUUID); + fetchAndDisplayStats(); + return true; + }); + category.addPreference(privateUserId); + + + apiUrl = new Preference(context); + apiUrl.setTitle(str("sb_general_api_url")); + apiUrl.setSummary(Html.fromHtml(str("sb_general_api_url_sum"))); + apiUrl.setOnPreferenceClickListener(preference1 -> { + EditText editText = new EditText(context); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + editText.setText(SettingsEnum.SB_API_URL.getString()); + + DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { + if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { + SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.getDefaultValue()); + ReVancedUtils.showToastLong(str("sb_api_url_reset")); + } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { + String serverAddress = editText.getText().toString(); + if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) { + ReVancedUtils.showToastLong(str("sb_api_url_invalid")); + } else if (!serverAddress.equals(SettingsEnum.SB_API_URL.getString())) { + SettingsEnum.SB_API_URL.saveValue(serverAddress); + ReVancedUtils.showToastLong(str("sb_api_url_changed")); + } + } + }; + new AlertDialog.Builder(context) + .setTitle(apiUrl.getTitle()) + .setView(editText) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("sb_reset"), urlChangeListener) + .setPositiveButton(android.R.string.ok, urlChangeListener) + .show(); + return true; + }); + category.addPreference(apiUrl); + + + importExport = new EditTextPreference(context); + importExport.setTitle(str("sb_settings_ie")); + importExport.setSummary(str("sb_settings_ie_sum")); + importExport.setOnPreferenceClickListener(preference1 -> { + importExport.getEditText().setText(SponsorBlockSettings.exportSettings()); + return true; + }); + importExport.setOnPreferenceChangeListener((preference1, newValue) -> { + SponsorBlockSettings.importSettings((String) newValue); + updateSegmentCategories(); + fetchAndDisplayStats(); + updateUI(); + return true; + }); + category.addPreference(importExport); } - private void addStatsCategory(Context context, PreferenceScreen screen) { - PreferenceCategory category = new PreferenceCategory(context); - screen.addPreference(category); - category.setTitle(str("stats")); - preferencesToDisableWhenSBDisabled.add(category); + private void updateSegmentCategories() { + try { + segmentCategory.removeAll(); - { - Preference preference = new Preference(context); - category.addPreference(preference); - preference.setTitle(str("stats_loading")); - preference.setSelectable(false); - - SBRequester.retrieveUserStats(category, preference); + Activity activity = getActivity(); + for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { + segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category)); + } + } catch (Exception ex) { + LogHelper.printException(() -> "updateSegmentCategories failure", ex); } } private void addAboutCategory(Context context, PreferenceScreen screen) { PreferenceCategory category = new PreferenceCategory(context); screen.addPreference(category); - category.setTitle(str("about")); + category.setTitle(str("sb_about")); { Preference preference = new Preference(context); screen.addPreference(preference); - preference.setTitle(str("about_api")); - preference.setSummary(str("about_api_sum")); + preference.setTitle(str("sb_about_api")); + preference.setSummary(str("sb_about_api_sum")); preference.setOnPreferenceClickListener(preference1 -> { Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse("https://sponsor.ajay.app")); @@ -212,197 +379,160 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment implements { Preference preference = new Preference(context); screen.addPreference(preference); - preference.setTitle(str("about_madeby")); + preference.setSummary(str("sb_about_made_by")); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + preference.setSingleLineTitle(false); + } preference.setSelectable(false); } - } - private void addGeneralCategory(final Context context, PreferenceScreen screen) { - final PreferenceCategory category = new PreferenceCategory(context); - preferencesToDisableWhenSBDisabled.add(category); - screen.addPreference(category); - category.setTitle(str("general")); - - { - Preference preference = new Preference(context); - preference.setTitle(str("sb_guidelines_preference_title")); - preference.setSummary(str("sb_guidelines_preference_sum")); - preference.setOnPreferenceClickListener(preference1 -> { - openGuidelines(); - return false; - }); - screen.addPreference(preference); - } - - { - SwitchPreference preference = new SwitchPreference(context); - preference.setTitle(str("general_skiptoast")); - preference.setSummary(str("general_skiptoast_sum")); - preference.setChecked(SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean()); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.saveValue(newValue); - return true; - }); - preference.setOnPreferenceClickListener(preference12 -> { - Toast.makeText(preference12.getContext(), str("skipped_sponsor"), Toast.LENGTH_SHORT).show(); - return false; - }); - preferencesToDisableWhenSBDisabled.add(preference); - screen.addPreference(preference); - } - - { - SwitchPreference preference = new SwitchPreference(context); - preference.setTitle(str("general_skipcount")); - preference.setSummary(str("general_skipcount_sum")); - preference.setChecked(SettingsEnum.SB_COUNT_SKIPS.getBoolean()); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_COUNT_SKIPS.saveValue(newValue); - return true; - }); - preferencesToDisableWhenSBDisabled.add(preference); - screen.addPreference(preference); - } - - { - SwitchPreference preference = new SwitchPreference(context); - preference.setTitle(str("general_time_without_sb")); - preference.setSummary(str("general_time_without_sb_sum")); - preference.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue); - return true; - }); - - preferencesToDisableWhenSBDisabled.add(preference); - screen.addPreference(preference); - } - - { - EditTextPreference preference = new EditTextPreference(context); - preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - preference.setTitle(str("general_adjusting")); - preference.setSummary(str("general_adjusting_sum")); - preference.setText(String.valueOf(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt())); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(Integer.valueOf(newValue.toString())); - return true; - }); - screen.addPreference(preference); - preferencesToDisableWhenSBDisabled.add(preference); - } - - { - EditTextPreference preference = new EditTextPreference(context); - preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - preference.setTitle(str("general_min_duration")); - preference.setSummary(str("general_min_duration_sum")); - preference.setText(String.valueOf(SettingsEnum.SB_MIN_DURATION.getFloat())); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString())); - return true; - }); - screen.addPreference(preference); - preferencesToDisableWhenSBDisabled.add(preference); - } - - { - EditTextPreference preference = new EditTextPreference(context); - preference.setTitle(str("general_uuid")); - preference.setSummary(str("general_uuid_sum")); - preference.setText(SettingsEnum.SB_UUID.getString()); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SettingsEnum.SB_UUID.saveValue(newValue.toString()); - return true; - }); - - screen.addPreference(preference); - preferencesToDisableWhenSBDisabled.add(preference); - } - - { - Preference preference = new Preference(context); - String title = str("general_api_url"); - preference.setTitle(title); - preference.setSummary(Html.fromHtml(str("general_api_url_sum"))); - preference.setOnPreferenceClickListener(preference1 -> { - EditText editText = new EditText(context); - editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); - editText.setText(SettingsEnum.SB_API_URL.getString()); - - API_URL_CHANGE_LISTENER.setEditTextRef(editText); - new AlertDialog.Builder(context) - .setTitle(title) - .setView(editText) - .setNegativeButton(android.R.string.cancel, null) - .setNeutralButton(str("reset"), API_URL_CHANGE_LISTENER) - .setPositiveButton(android.R.string.ok, API_URL_CHANGE_LISTENER) - .show(); - return true; - }); - - screen.addPreference(preference); - preferencesToDisableWhenSBDisabled.add(preference); - } - - { - EditTextPreference preference = new EditTextPreference(context); - Context applicationContext = context.getApplicationContext(); - - preference.setTitle(str("settings_ie")); - preference.setSummary(str("settings_ie_sum")); - preference.setText(SponsorBlockUtils.exportSettings(applicationContext)); - preference.setOnPreferenceChangeListener((preference1, newValue) -> { - SponsorBlockUtils.importSettings((String) newValue, applicationContext); - return false; - }); - screen.addPreference(preference); - preferencesToDisableWhenSBDisabled.add(preference); - } + private void openGuidelines() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines")); + getActivity().startActivity(intent); } - private static class APIURLChangeListener implements DialogInterface.OnClickListener { - private WeakReference editTextRef; - - @Override - public void onClick(DialogInterface dialog, int which) { - EditText editText = editTextRef.get(); - if (editText == null) - return; - Context context = ((AlertDialog) dialog).getContext(); - Context applicationContext = context.getApplicationContext(); - - switch (which) { - case DialogInterface.BUTTON_NEUTRAL: - SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.getDefaultValue()); - Toast.makeText(applicationContext, str("api_url_reset"), Toast.LENGTH_SHORT).show(); - break; - case DialogInterface.BUTTON_POSITIVE: - Editable text = editText.getText(); - Toast invalidToast = Toast.makeText(applicationContext, str("api_url_invalid"), Toast.LENGTH_SHORT); - if (text == null) { - invalidToast.show(); - } else { - String textAsString = text.toString(); - if (textAsString.isEmpty() || !Patterns.WEB_URL.matcher(textAsString).matches()) { - invalidToast.show(); - } else { - SettingsEnum.SB_API_URL.saveValue(textAsString); - Toast.makeText(applicationContext, str("api_url_changed"), Toast.LENGTH_SHORT).show(); - } - } - break; + private void fetchAndDisplayStats() { + try { + statsCategory.removeAll(); + Preference loadingPlaceholderPreference = new Preference(this.getActivity()); + loadingPlaceholderPreference.setEnabled(false); + statsCategory.addPreference(loadingPlaceholderPreference); + if (SettingsEnum.SB_ENABLED.getBoolean()) { + loadingPlaceholderPreference.setTitle(str("sb_stats_loading")); + ReVancedUtils.runOnBackgroundThread(() -> { + UserStats stats = SBRequester.retrieveUserStats(); + ReVancedUtils.runOnMainThread(() -> { // get back on main thread to modify UI elements + addUserStats(loadingPlaceholderPreference, stats); + }); + }); + } else { + loadingPlaceholderPreference.setTitle(str("sb_stats_sb_disabled")); } - } - - public void setEditTextRef(EditText editText) { - editTextRef = new WeakReference<>(editText); + } catch (Exception ex) { + LogHelper.printException(() -> "fetchAndDisplayStats failure", ex); } } - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - SponsorBlockSettings.update(getActivity()); + private static final DecimalFormat statsNumberOfSegmentsSkippedFormatter = new DecimalFormat("#,###,###"); + + private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) { + ReVancedUtils.verifyOnMainThread(); + try { + if (stats == null) { + loadingPlaceholder.setTitle(str("sb_stats_connection_failure")); + return; + } + statsCategory.removeAll(); + Context context = statsCategory.getContext(); + + { + EditTextPreference preference = new EditTextPreference(context); + statsCategory.addPreference(preference); + String userName = stats.userName; + preference.setTitle(fromHtml(str("sb_stats_username", userName))); + preference.setSummary(str("sb_stats_username_change")); + preference.setText(userName); + preference.setOnPreferenceChangeListener((preference1, value) -> { + ReVancedUtils.runOnBackgroundThread(() -> { + String newUserName = (String) value; + String errorMessage = SBRequester.setUsername(newUserName); + ReVancedUtils.runOnMainThread(() -> { + if (errorMessage == null) { + preference.setTitle(fromHtml(str("sb_stats_username", newUserName))); + preference.setText(newUserName); + ReVancedUtils.showToastLong(str("sb_stats_username_changed")); + } else { + preference.setText(userName); // revert to previous + ReVancedUtils.showToastLong(errorMessage); + } + }); + }); + return true; + }); + } + + { + // number of segment submissions (does not include ignored segments) + Preference preference = new Preference(context); + statsCategory.addPreference(preference); + String formatted = statsNumberOfSegmentsSkippedFormatter.format(stats.segmentCount); + preference.setTitle(fromHtml(str("sb_stats_submissions", formatted))); + if (stats.segmentCount == 0) { + preference.setSelectable(false); + } else { + preference.setOnPreferenceClickListener(preference1 -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId)); + preference1.getContext().startActivity(i); + return true; + }); + } + } + + { + // "user reputation". Usually not useful, since it appears most users have zero reputation. + // But if there is a reputation, then show it here + Preference preference = new Preference(context); + preference.setTitle(fromHtml(str("sb_stats_reputation", stats.reputation))); + preference.setSelectable(false); + if (stats.reputation != 0) { + statsCategory.addPreference(preference); + } + } + + { + // time saved for other users + Preference preference = new Preference(context); + statsCategory.addPreference(preference); + + String stats_saved; + String stats_saved_sum; + if (stats.segmentCount == 0) { + stats_saved = str("sb_stats_saved_zero"); + stats_saved_sum = str("sb_stats_saved_sum_zero"); + } else { + stats_saved = str("sb_stats_saved", statsNumberOfSegmentsSkippedFormatter.format(stats.viewCount)); + stats_saved_sum = str("sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved))); + } + preference.setTitle(fromHtml(stats_saved)); + preference.setSummary(fromHtml(stats_saved_sum)); + preference.setOnPreferenceClickListener(preference1 -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://sponsor.ajay.app/stats/")); + preference1.getContext().startActivity(i); + return false; + }); + } + + { + // time the user saved by using SB + Preference preference = new Preference(context); + statsCategory.addPreference(preference); + + Runnable updateStatsSelfSaved = () -> { + String formatted = statsNumberOfSegmentsSkippedFormatter.format(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt()); + preference.setTitle(fromHtml(str("sb_stats_self_saved", formatted))); + String formattedSaved = SponsorBlockUtils.getTimeSavedString(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / 1000); + preference.setSummary(fromHtml(str("sb_stats_self_saved_sum", formattedSaved))); + }; + updateStatsSelfSaved.run(); + preference.setOnPreferenceClickListener(preference1 -> { + new AlertDialog.Builder(preference1.getContext()) + .setTitle(str("sb_stats_self_saved_reset_title")) + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getDefaultValue()); + SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getDefaultValue()); + updateStatsSelfSaved.run(); + }) + .setNegativeButton(android.R.string.no, null).show(); + return true; + }); + } + } catch (Exception ex) { + LogHelper.printException(() -> "fetchAndDisplayStats failure", ex); + } } + } diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt index b588665a..1db019e1 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerControlsVisibilityObserver.kt @@ -19,13 +19,13 @@ class PlayerControlsVisibilityObserverImpl( * id of the direct parent of controls_layout, R.id.youtube_controls_overlay */ private val controlsLayoutParentId = - ReVancedUtils.getResourceIdByName(activity, "id", "youtube_controls_overlay") + ReVancedUtils.getResourceIdentifier(activity, "youtube_controls_overlay", "id") /** * id of R.id.controls_layout */ private val controlsLayoutId = - ReVancedUtils.getResourceIdByName(activity, "id", "controls_layout") + ReVancedUtils.getResourceIdentifier(activity, "controls_layout", "id") /** * reference to the controls layout view diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt index d973af2f..371767c5 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -7,8 +7,8 @@ import app.revanced.integrations.utils.Event */ @Suppress("unused") enum class PlayerType { - NONE, // includes Shorts playback - HIDDEN, // also includes YouTube Shorts and Stories, if a regular video is minimized and a Short/Story is then opened + NONE, // includes Shorts and Stories playback + HIDDEN, // A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened WATCH_WHILE_MINIMIZED, WATCH_WHILE_MAXIMIZED, WATCH_WHILE_FULLSCREEN, @@ -48,6 +48,7 @@ enum class PlayerType { /** * player type change listener */ + @JvmStatic val onChange = Event() } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/InjectedPlugin.java b/app/src/main/java/app/revanced/integrations/sponsorblock/InjectedPlugin.java deleted file mode 100644 index 1d03be84..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/InjectedPlugin.java +++ /dev/null @@ -1,97 +0,0 @@ -package app.revanced.integrations.sponsorblock; - - -import android.view.View; -import android.view.ViewGroup; - -import java.lang.reflect.Field; - -import app.revanced.integrations.utils.LogHelper; - -// invoke-static {p0}, Lpl/jakubweg/InjectedPlugin;->inject(Landroid/content/Context;)V -// invoke-static {}, Lpl/jakubweg/InjectedPlugin;->printSomething()V -// InlineTimeBar -public class InjectedPlugin { - - public static void printSomething() { - LogHelper.printDebug(() -> "printSomething called"); - } - - public static void printObject(Object o, int recursive) { - if (o == null) - LogHelper.printDebug(() -> "Printed object is null"); - else { - LogHelper.printDebug(() -> "Printed object (" - + o.getClass().getName() - + ") = " + o.toString()); - for (Field field : o.getClass().getDeclaredFields()) { - if (field.getType().isPrimitive()) - continue; - field.setAccessible(true); - try { - Object value = field.get(o); - try { -// if ("java.lang.String".equals(field.getType().getName())) - LogHelper.printDebug(() -> "Field: " + field.toString() + " has value " + value); - } catch (Exception e) { - LogHelper.printDebug(() -> "Field: " + field.toString() + " has value that thrown an exception in toString method"); - } - if (recursive > 0 && value != null && !value.getClass().isPrimitive()) - printObject(value, recursive - 1); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - } - } - - public static void printObject(Object o) { - printObject(o, 0); - } - - public static void printObject(int o) { - printObject(Integer.valueOf(o)); - } - - public static void printObject(float o) { - printObject(Float.valueOf(o)); - } - - public static void printObject(long o) { - printObject(Long.valueOf(o)); - } - - public static void printStackTrace() { - StackTraceElement[] stackTrace = (new Throwable()).getStackTrace(); - LogHelper.printDebug(() -> "Printing stack trace:"); - for (StackTraceElement element : stackTrace) { - LogHelper.printDebug(() -> element.toString()); - } - } - - public static void printViewStack(final View view, int spaces) { - StringBuilder builder = new StringBuilder(spaces); - for (int i = 0; i < spaces; i++) { - builder.append('-'); - } - String spacesStr = builder.toString(); - - if (view == null) { - LogHelper.printDebug(() -> spacesStr + "Null view"); - return; - } - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - LogHelper.printDebug(() -> spacesStr + "View group: " + view); - int childCount = group.getChildCount(); - LogHelper.printDebug(() -> spacesStr + "Children count: " + childCount); - for (int i = 0; i < childCount; i++) { - printViewStack(group.getChildAt(i), spaces + 1); - } - } else { - LogHelper.printDebug(() -> spacesStr + "Normal view: " + view); - } - } -} - - diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/NewSegmentHelperLayout.java b/app/src/main/java/app/revanced/integrations/sponsorblock/NewSegmentHelperLayout.java deleted file mode 100644 index 61a5957d..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/NewSegmentHelperLayout.java +++ /dev/null @@ -1,28 +0,0 @@ -package app.revanced.integrations.sponsorblock; - -import android.content.Context; - -import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.hideNewSegmentLayout; -import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.showNewSegmentLayout; - -public class NewSegmentHelperLayout { - public static Context context; - private static boolean isShown = false; - - public static void show() { - if (isShown) return; - isShown = true; - showNewSegmentLayout(); - } - - public static void hide() { - if (!isShown) return; - isShown = false; - hideNewSegmentLayout(); - } - - public static void toggle() { - if (isShown) hide(); - else show(); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/PlayerController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/PlayerController.java deleted file mode 100644 index 713d38fa..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/PlayerController.java +++ /dev/null @@ -1,431 +0,0 @@ -package app.revanced.integrations.sponsorblock; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; -import app.revanced.integrations.patches.VideoInformation; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.shared.PlayerType; -import app.revanced.integrations.sponsorblock.objects.SponsorSegment; -import app.revanced.integrations.sponsorblock.requests.SBRequester; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Timer; -import java.util.TimerTask; - -import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.timeWithoutSegments; -import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.videoHasSegments; - -public class PlayerController { - - private static final Timer sponsorTimer = new Timer("sponsor-skip-timer"); - public static WeakReference playerActivity = new WeakReference<>(null); - public static SponsorSegment[] sponsorSegmentsOfCurrentVideo; - private static long allowNextSkipRequestTime = 0L; - private static String currentVideoId; - private static long lastKnownVideoTime = -1L; - private static final Runnable findAndSkipSegmentRunnable = () -> { - findAndSkipSegment(false); - }; - private static float sponsorBarLeft = 1f; - private static float sponsorBarRight = 1f; - private static float sponsorBarThickness = 2f; - private static TimerTask skipSponsorTask = null; - - public static String getCurrentVideoId() { - return currentVideoId; - } - - public static void setCurrentVideoId(final String videoId) { - try { - if (videoId == null) { - currentVideoId = null; - sponsorSegmentsOfCurrentVideo = null; - return; - } - - // currently this runs every time a video is loaded (regardless if sponsorblock is turned on or off) - // FIXME: change this so if sponsorblock is disabled, then run this method exactly once and once only - SponsorBlockSettings.update(null); - - if (!SettingsEnum.SB_ENABLED.getBoolean()) { - currentVideoId = null; - return; - } - if (PlayerType.getCurrent() == PlayerType.NONE) { - LogHelper.printDebug(() -> "ignoring shorts video"); - currentVideoId = null; - return; - } - if (videoId.equals(currentVideoId)) - return; - - currentVideoId = videoId; - sponsorSegmentsOfCurrentVideo = null; - LogHelper.printDebug(() -> "setCurrentVideoId: videoId=" + videoId); - - sponsorTimer.schedule(new TimerTask() { - @Override - public void run() { - try { - executeDownloadSegments(currentVideoId); - } catch (Exception e) { - LogHelper.printException(() -> "Failed to download segments", e); - } - } - }, 0); - } catch (Exception ex) { - LogHelper.printException(() -> "setCurrentVideoId failure", ex); - } - } - - /** - * Called when creating some kind of youtube internal player controlled, every time when new video starts to play - */ - public static void initialize(Object _o) { - try { - lastKnownVideoTime = 0; - SkipSegmentView.hide(); - NewSegmentHelperLayout.hide(); - } catch (Exception ex) { - LogHelper.printException(() -> "initialize failure", ex); - } - } - - public static void executeDownloadSegments(String videoId) { - try { - videoHasSegments = false; - timeWithoutSegments = ""; - - SponsorSegment[] segments = SBRequester.getSegments(videoId); - Arrays.sort(segments); - - for (SponsorSegment segment : segments) { - LogHelper.printDebug(() -> "Detected segment: " + segment.toString()); - } - - sponsorSegmentsOfCurrentVideo = segments; - // new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable); - } catch (Exception ex) { - LogHelper.printException(() -> "executeDownloadSegments failure", ex); - } - } - - - public static void setVideoTime(long millis) { - try { - if (!SettingsEnum.SB_ENABLED.getBoolean()) return; - LogHelper.printDebug(() -> "setCurrentVideoTime: current video time: " + millis); - // fixme? if (millis == lastKnownVideoTime), should it return here and not continue? - lastKnownVideoTime = millis; - if (millis <= 0) return; - //findAndSkipSegment(false); - - if (millis == VideoInformation.getCurrentVideoLength()) { - SponsorBlockUtils.hideShieldButton(); - SponsorBlockUtils.hideVoteButton(); - return; - } - - SponsorSegment[] segments = sponsorSegmentsOfCurrentVideo; - if (segments == null || segments.length == 0) return; - - final long START_TIMER_BEFORE_SEGMENT_MILLIS = 1200; - final long startTimerAtMillis = millis + START_TIMER_BEFORE_SEGMENT_MILLIS; - - for (final SponsorSegment segment : segments) { - if (segment.start > millis) { - if (segment.start > startTimerAtMillis) - break; // it's more then START_TIMER_BEFORE_SEGMENT_MILLIS far away - if (!segment.category.behaviour.skip) - break; - - if (skipSponsorTask == null) { - LogHelper.printDebug(() -> "Scheduling skipSponsorTask"); - skipSponsorTask = new TimerTask() { - @Override - public void run() { - skipSponsorTask = null; - lastKnownVideoTime = segment.start + 1; - ReVancedUtils.runOnMainThread(findAndSkipSegmentRunnable); - } - }; - sponsorTimer.schedule(skipSponsorTask, segment.start - millis); - } else { - LogHelper.printDebug(() -> "skipSponsorTask is already scheduled..."); - } - - break; - } - - if (segment.end < millis) - continue; - - // we are in the segment! - if (segment.category.behaviour.skip && !(segment.category.behaviour.key.equals("skip-once") && segment.didAutoSkipped)) { - sendViewRequestAsync(millis, segment); - skipSegment(segment, false); - break; - } else { - SkipSegmentView.show(); - return; - } - } - SkipSegmentView.hide(); - } catch (Exception e) { - LogHelper.printException(() -> "setVideoTime failure", e); - } - } - - private static void sendViewRequestAsync(final long millis, final SponsorSegment segment) { - if (segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED) { - Context context = ReVancedUtils.getContext(); - if (context != null) { - long newSkippedTime = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() + (segment.end - segment.start); - SettingsEnum.SB_SKIPPED_SEGMENTS.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS.getInt() + 1); - SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.saveValue(newSkippedTime); - } - } - if (SettingsEnum.SB_COUNT_SKIPS.getBoolean() - && segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED - && millis - segment.start < 2000) { // Only skips from the start should count as a view - ReVancedUtils.runOnBackgroundThread(() -> { - SBRequester.sendViewCountRequest(segment); - }); - } - } - - public static void setHighPrecisionVideoTime(final long millis) { - try { - if ((millis < lastKnownVideoTime && lastKnownVideoTime >= VideoInformation.getCurrentVideoLength()) || millis == 0) { - SponsorBlockUtils.showShieldButton(); // skipping from end to the video will show the buttons again - SponsorBlockUtils.showVoteButton(); - } - if (lastKnownVideoTime > 0) { - lastKnownVideoTime = millis; - } else - setVideoTime(millis); - } catch (Exception ex) { - LogHelper.printException(() -> "setHighPrecisionVideoTime failure", ex); - } - } - - public static long getCurrentVideoLength() { - return VideoInformation.getCurrentVideoLength(); - } - - public static long getLastKnownVideoTime() { - return lastKnownVideoTime; - } - - public static void setSponsorBarAbsoluteLeft(final Rect rect) { - setSponsorBarAbsoluteLeft(rect.left); - } - - public static void setSponsorBarAbsoluteLeft(final float left) { - LogHelper.printDebug(() -> String.format("setSponsorBarLeft: left=%.2f", left)); - - sponsorBarLeft = left; - } - - public static void setSponsorBarRect(final Object self) { - try { - Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect"); - field.setAccessible(true); - Rect rect = (Rect) field.get(self); - if (rect != null) { - setSponsorBarAbsoluteLeft(rect.left); - setSponsorBarAbsoluteRight(rect.right); - } - } catch (Exception ex) { - LogHelper.printException(() -> "setSponsorBarRect failure", ex); - } - } - - public static void setSponsorBarAbsoluteRight(final Rect rect) { - setSponsorBarAbsoluteRight(rect.right); - } - - public static void setSponsorBarAbsoluteRight(final float right) { - LogHelper.printDebug(() -> String.format("setSponsorBarRight: right=%.2f", right)); - - sponsorBarRight = right; - } - - public static void setSponsorBarThickness(final int thickness) { - try { - setSponsorBarThickness((float) thickness); - } catch (Exception ex) { - LogHelper.printException(() -> "setSponsorBarThickness failure", ex); - } - } - - public static void setSponsorBarThickness(final float thickness) { -// if (VERBOSE_DRAW_OPTIONS) -// LogH(PlayerController.class, String.format("setSponsorBarThickness: thickness=%.2f", thickness)); - - sponsorBarThickness = thickness; - } - - public static void onSkipSponsorClicked() { - LogHelper.printDebug(() -> "Skip segment clicked"); - findAndSkipSegment(true); - } - - - public static void addSkipSponsorView15(final View view) { - try { - playerActivity = new WeakReference<>((Activity) view.getContext()); - LogHelper.printDebug(() -> "addSkipSponsorView15: view=" + view.toString()); - - ReVancedUtils.runOnMainThreadDelayed(() -> { - final ViewGroup viewGroup = (ViewGroup) ((ViewGroup) view).getChildAt(2); - Activity context = ((Activity) viewGroup.getContext()); - NewSegmentHelperLayout.context = context; - }, 500); - } catch (Exception ex) { - LogHelper.printException(() -> "addSkipSponsorView15 failure", ex); - } - } - - // Edit: Is this method ever called? Where is the patch code that calls this? - public static void addSkipSponsorView14(final View view) { - try { - playerActivity = new WeakReference<>((Activity) view.getContext()); - LogHelper.printDebug(() -> "addSkipSponsorView14: view=" + view.toString()); - ReVancedUtils.runOnMainThreadDelayed(() -> { - final ViewGroup viewGroup = (ViewGroup) view.getParent(); - Activity activity = (Activity) viewGroup.getContext(); - NewSegmentHelperLayout.context = activity; - }, 500); - } catch (Exception ex) { - LogHelper.printException(() -> "addSkipSponsorView14 failure", ex); - } - } - - - /** - * Called when it's time to draw time bar - */ - public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { - try { - if (sponsorBarThickness < 0.1) return; - if (sponsorSegmentsOfCurrentVideo == null) return; - - - final float thicknessDiv2 = sponsorBarThickness / 2; - final float top = posY - thicknessDiv2; - final float bottom = posY + thicknessDiv2; - final float absoluteLeft = sponsorBarLeft; - final float absoluteRight = sponsorBarRight; - - final float tmp1 = 1f / (float) VideoInformation.getCurrentVideoLength() * (absoluteRight - absoluteLeft); - for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) { - float left = segment.start * tmp1 + absoluteLeft; - float right = segment.end * tmp1 + absoluteLeft; - canvas.drawRect(left, top, right, bottom, segment.category.paint); - } - } catch (Exception ex) { - LogHelper.printException(() -> "drawSponsorTimeBars failure", ex); - } - } - - // private final static Pattern videoIdRegex = Pattern.compile(".*\\.be\\/([A-Za-z0-9_\\-]{0,50}).*"); - public static String substringVideoIdFromLink(String link) { - return link.substring(link.lastIndexOf('/') + 1); - } - - public static void skipRelativeMilliseconds(int millisRelative) { - skipToMillisecond(lastKnownVideoTime + millisRelative); - } - - public static boolean skipToMillisecond(long millisecond) { - // in 15.x if sponsor clip hits the end, then it crashes the app, because of too many function invocations - // I put this block so that skip can be made only once per some time - long now = System.currentTimeMillis(); - if (now < allowNextSkipRequestTime) { - LogHelper.printDebug(() -> "skipToMillisecond: to fast, slow down, because you'll fail"); - return false; - } - allowNextSkipRequestTime = now + 100; - - LogHelper.printDebug(() -> String.format("Requesting skip to millis=%d on thread %s", millisecond, Thread.currentThread().toString())); - - final long finalMillisecond = millisecond; - - try { - LogHelper.printDebug(() -> "Skipping to millis=" + finalMillisecond); - lastKnownVideoTime = finalMillisecond; - VideoInformation.seekTo(finalMillisecond); - } catch (Exception e) { - LogHelper.printException(() -> "Cannot skip to millisecond", e); - } - - return true; - } - - - private static void findAndSkipSegment(boolean wasClicked) { - try { - if (sponsorSegmentsOfCurrentVideo == null) - return; - - final long millis = lastKnownVideoTime; - - for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) { - if (segment.start > millis) - break; - - if (segment.end < millis) - continue; - - SkipSegmentView.show(); - if (!((segment.category.behaviour.skip && !(segment.category.behaviour.key.equals("skip-once") && segment.didAutoSkipped)) || wasClicked)) - return; - - sendViewRequestAsync(millis, segment); - skipSegment(segment, wasClicked); - break; - } - - SkipSegmentView.hide(); - } catch (Exception ex) { - LogHelper.printException(() -> "findAndSkipSegment failure", ex); - } - } - - private static void skipSegment(SponsorSegment segment, boolean wasClicked) { - try { -// if (lastSkippedSegment == segment) return; -// lastSkippedSegment = segment; - LogHelper.printDebug(() -> "Skipping segment: " + segment.toString()); - - if (SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean() && !wasClicked) - SkipSegmentView.notifySkipped(segment); - - boolean didSucceed = skipToMillisecond(segment.end + 2); - if (didSucceed && !wasClicked) { - segment.didAutoSkipped = true; - } - SkipSegmentView.hide(); - if (segment.category == SponsorBlockSettings.SegmentInfo.UNSUBMITTED) { - SponsorSegment[] newSegments = new SponsorSegment[sponsorSegmentsOfCurrentVideo.length - 1]; - int i = 0; - for (SponsorSegment sponsorSegment : sponsorSegmentsOfCurrentVideo) { - if (sponsorSegment != segment) - newSegments[i++] = sponsorSegment; - } - sponsorSegmentsOfCurrentVideo = newSegments; - } - } catch (Exception ex) { - LogHelper.printException(() -> "skipSegment failure", ex); - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java new file mode 100644 index 00000000..5435bfe0 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java @@ -0,0 +1,595 @@ +package app.revanced.integrations.sponsorblock; + +import static app.revanced.integrations.utils.StringRef.str; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.shared.PlayerType; +import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; +import app.revanced.integrations.sponsorblock.objects.SegmentCategory; +import app.revanced.integrations.sponsorblock.objects.SponsorSegment; +import app.revanced.integrations.sponsorblock.requests.SBRequester; +import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +/** + * Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video. + * + * Class is not thread safe. All methods must be called on the main thread unless otherwise specified. + */ +public class SegmentPlaybackController { + @Nullable + private static String currentVideoId; + @Nullable + private static SponsorSegment[] segmentsOfCurrentVideo; + + /** + * Current segment that user can manually skip + */ + @Nullable + private static SponsorSegment segmentCurrentlyPlaying; + /** + * Currently playing manual skip segment, that is scheduled to hide. + * This will always be NULL or equal to {@link #segmentCurrentlyPlaying} + */ + @Nullable + private static SponsorSegment scheduledHideSegment; + /** + * Upcoming segment that is scheduled to either autoskip or show the manual skip button + */ + @Nullable + private static SponsorSegment scheduledUpcomingSegment; + + @Nullable + private static String timeWithoutSegments; + + private static float sponsorBarLeft = 1f; + private static float sponsorBarRight = 1f; + private static float sponsorBarThickness = 2f; + + @Nullable + public static SponsorSegment[] getSegmentsOfCurrentVideo() { + return segmentsOfCurrentVideo; + } + + static void setSegmentsOfCurrentVideo(@NonNull SponsorSegment[] segments) { + Arrays.sort(segments); + segmentsOfCurrentVideo = segments; + calculateTimeWithoutSegments(); + } + + public static boolean currentVideoHasSegments() { + return segmentsOfCurrentVideo != null && segmentsOfCurrentVideo.length > 0; + } + + @Nullable + static String getCurrentVideoId() { + return currentVideoId; + } + + /** + * Clears all downloaded data + */ + private static void clearData() { + currentVideoId = null; + segmentsOfCurrentVideo = null; + timeWithoutSegments = null; + segmentCurrentlyPlaying = null; + scheduledUpcomingSegment = null; // prevent any existing scheduled skip from running + scheduledHideSegment = null; + toastSegmentSkipped = null; // prevent any scheduled skip toasts from showing + toastNumberOfSegmentsSkipped = 0; + } + + /** + * Injection point. + * Initializes SponsorBlock when the video player starts playing a new video. + */ + public static void initialize(Object _o) { + try { + ReVancedUtils.verifyOnMainThread(); + SponsorBlockSettings.initialize(); + clearData(); + SponsorBlockViewController.hideSkipButton(); + SponsorBlockViewController.hideNewSegmentLayout(); + SponsorBlockUtils.clearUnsubmittedSegmentTimes(); + LogHelper.printDebug(() -> "Initialized SponsorBlock"); + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to initialize SponsorBlock", ex); + } + } + + /** + * Injection point. + */ + public static void setCurrentVideoId(@Nullable String videoId) { + try { + if (Objects.equals(currentVideoId, videoId)) { + return; + } + clearData(); + if (videoId == null || !SettingsEnum.SB_ENABLED.getBoolean()) { + return; + } + if (PlayerType.getCurrent().isNoneOrHidden()) { + LogHelper.printDebug(() -> "ignoring short or story"); + return; + } + if (!ReVancedUtils.isNetworkConnected()) { + LogHelper.printDebug(() -> "Network not connected, ignoring video"); + return; + } + + currentVideoId = videoId; + LogHelper.printDebug(() -> "setCurrentVideoId: " + videoId); + + //noinspection UnnecessaryLocalVariable + String videoIdToDownload = videoId; // make a copy, to use off main thread + ReVancedUtils.runOnBackgroundThread(() -> { + try { + executeDownloadSegments(videoIdToDownload); + } catch (Exception e) { + LogHelper.printException(() -> "Failed to download segments", e); + } + }); + } catch (Exception ex) { + LogHelper.printException(() -> "setCurrentVideoId failure", ex); + } + } + + /** + * Must be called off main thread + */ + static void executeDownloadSegments(@NonNull String videoId) { + Objects.requireNonNull(videoId); + try { + SponsorSegment[] segments = SBRequester.getSegments(videoId); + + ReVancedUtils.runOnMainThread(()-> { + if (!videoId.equals(currentVideoId)) { + // user changed videos before get segments network call could complete + LogHelper.printDebug(() -> "Ignoring segments for prior video: " + videoId); + return; + } + setSegmentsOfCurrentVideo(segments); + setVideoTime(VideoInformation.getVideoTime()); // check for any skips now, instead of waiting for the next update + }); + } catch (Exception ex) { + LogHelper.printException(() -> "executeDownloadSegments failure", ex); + } + } + + /** + * Injection point. + * Updates SponsorBlock every 1000ms. + * When changing videos, this is first called with value 0 and then the video is changed. + */ + public static void setVideoTime(long millis) { + try { + if (!SettingsEnum.SB_ENABLED.getBoolean() + || PlayerType.getCurrent().isNoneOrHidden() // shorts playback + || segmentsOfCurrentVideo == null || segmentsOfCurrentVideo.length == 0) { + return; + } + LogHelper.printDebug(() -> "setVideoTime: " + millis); + + // to debug the timing logic, set this to a very large value (5000 or more) + // then try manually seeking just playback reaches a skip/hide of different segments + final long lookAheadMilliseconds = 1500; // must be larger than the average time between calls to this method + final float playbackSpeed = RememberPlaybackSpeedPatch.getCurrentPlaybackSpeed(); + final long startTimerLookAheadThreshold = millis + (long)(playbackSpeed * lookAheadMilliseconds); + + SponsorSegment foundCurrentSegment = null; + SponsorSegment foundUpcomingSegment = null; + + for (final SponsorSegment segment : segmentsOfCurrentVideo) { + if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR + || segment.category.behaviour == CategoryBehaviour.IGNORE) { + continue; + } + if (segment.end <= millis) { + continue; // past this segment + } + + if (segment.start <= millis) { + // we are in the segment! + if (segment.shouldAutoSkip()) { + skipSegment(segment, false); + return; // must return, as skipping causes a recursive call back into this method + } + + // first found segment, or it's an embedded segment and fully inside the outer segment + if (foundCurrentSegment == null || foundCurrentSegment.containsSegment(segment)) { + // If the found segment is not currently displayed, then do not show if the segment is nearly over. + // This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time. + // Also prevents showing the skip button if user seeks into the last half second of the segment. + final long minMillisOfSegmentRemainingThreshold = 500; + if (segmentCurrentlyPlaying == segment + || !segment.timeIsNearEnd(millis, minMillisOfSegmentRemainingThreshold)) { + foundCurrentSegment = segment; + } else { + LogHelper.printDebug(() -> "Ignoring segment that ends very soon: " + segment); + } + } + // Keep iterating and looking. There may be an upcoming autoskip, + // or there may be another smaller segment nested inside this segment + continue; + } + + // segment is upcoming + if (startTimerLookAheadThreshold < segment.start) { + break; // segment is not close enough to schedule, and no segments after this are of interest + } + if (segment.shouldAutoSkip()) { // upcoming autoskip + foundUpcomingSegment = segment; + break; // must stop here + } + + // upcoming manual skip + + // do not schedule upcoming segment, if it is not fully contained inside the current segment + if ((foundCurrentSegment == null || foundCurrentSegment.containsSegment(segment)) + // use the most inner upcoming segment + && (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) { + + // Only schedule, if the segment start time is not near the end time of the current segment. + // This check is needed to prevent scheduled hide and show from clashing with each other. + final long minTimeBetweenStartEndOfSegments = 1000; + if (foundCurrentSegment == null + || !foundCurrentSegment.timeIsNearEnd(segment.start, minTimeBetweenStartEndOfSegments)) { + foundUpcomingSegment = segment; + } else { + LogHelper.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment); + } + } + } + + + if (segmentCurrentlyPlaying != foundCurrentSegment) { + if (foundCurrentSegment == null) { + LogHelper.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying); + segmentCurrentlyPlaying = null; + SponsorBlockViewController.hideSkipButton(); + } else { + segmentCurrentlyPlaying = foundCurrentSegment; + LogHelper.printDebug(() -> "Showing segment: " + segmentCurrentlyPlaying); + SponsorBlockViewController.showSkipButton(foundCurrentSegment); + } + } + + // must be greater than the average time between updates to VideoInformation time + final long videoInformationTimeUpdateThresholdMilliseconds = 250; + + // schedule a hide, only if the segment end is near + final SponsorSegment segmentToHide = + (foundCurrentSegment != null && foundCurrentSegment.timeIsNearEnd(millis, lookAheadMilliseconds)) + ? foundCurrentSegment + : null; + + if (scheduledHideSegment != segmentToHide) { + if (segmentToHide == null) { + LogHelper.printDebug(() -> "Clearing scheduled hide: " + scheduledHideSegment); + scheduledHideSegment = null; + } else { + scheduledHideSegment = segmentToHide; + LogHelper.printDebug(() -> "Scheduling hide segment: " + segmentToHide + " playbackSpeed: " + playbackSpeed); + final long delayUntilHide = (long) ((segmentToHide.end - millis) / playbackSpeed); + ReVancedUtils.runOnMainThreadDelayed(() -> { + if (scheduledHideSegment != segmentToHide) { + LogHelper.printDebug(() -> "Ignoring old scheduled hide segment: " + segmentToHide); + return; + } + scheduledHideSegment = null; + + final long videoTime = VideoInformation.getVideoTime(); + if (!segmentToHide.timeIsNearEnd(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) { + // current video time is not what's expected. User paused playback + LogHelper.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide + + " videoInformation time: " + videoTime); + return; + } + LogHelper.printDebug(() -> "Running scheduled hide segment: " + segmentToHide); + // Need more than just hide the skip button, as this may have been an embedded segment + // Instead call back into setVideoTime to check everything again. + // Should not use VideoInformation time as it is less accurate, + // but this scheduled handler was scheduled precisely so we can just use the segment end time + segmentCurrentlyPlaying = null; + SponsorBlockViewController.hideSkipButton(); + setVideoTime(segmentToHide.end); + }, delayUntilHide); + } + } + + if (scheduledUpcomingSegment != foundUpcomingSegment) { + if (foundUpcomingSegment == null) { + LogHelper.printDebug(() -> "Clearing scheduled segment: " + scheduledUpcomingSegment); + scheduledUpcomingSegment = null; + } else { + scheduledUpcomingSegment = foundUpcomingSegment; + final SponsorSegment segmentToSkip = foundUpcomingSegment; + + LogHelper.printDebug(() -> "Scheduling segment: " + segmentToSkip + " playbackSpeed: " + playbackSpeed); + final long delayUntilSkip = (long) ((segmentToSkip.start - millis) / playbackSpeed); + ReVancedUtils.runOnMainThreadDelayed(() -> { + if (scheduledUpcomingSegment != segmentToSkip) { + LogHelper.printDebug(() -> "Ignoring old scheduled segment: " + segmentToSkip); + return; + } + scheduledUpcomingSegment = null; + + final long videoTime = VideoInformation.getVideoTime(); + if (!segmentToSkip.timeIsNearStart(videoTime, + videoInformationTimeUpdateThresholdMilliseconds)) { + // current video time is not what's expected. User paused playback + LogHelper.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip + + " videoInformation time: " + videoTime); + return; + } + if (segmentToSkip.shouldAutoSkip()) { + LogHelper.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip); + skipSegment(segmentToSkip, false); + } else { + LogHelper.printDebug(() -> "Running scheduled show segment: " + segmentToSkip); + segmentCurrentlyPlaying = segmentToSkip; + SponsorBlockViewController.showSkipButton(segmentToSkip); + } + }, delayUntilSkip); + } + } + } catch (Exception e) { + LogHelper.printException(() -> "setVideoTime failure", e); + } + } + + + private static SponsorSegment lastSegmentSkipped; + private static long lastSegmentSkippedTime; + + private static void skipSegment(@NonNull SponsorSegment segment, boolean userManuallySkipped) { + try { + // If trying to seek to end of the video, YouTube can seek just short of the actual end. + // (especially if the video does not end on a whole second boundary). + // This causes additional segment skip attempts, even though it cannot seek any closer to the desired time. + // Check for and ignore repeated skip attempts of the same segment over a short time period. + final long now = System.currentTimeMillis(); + final long minimumMillisecondsBetweenSkippingSameSegment = 500; + if ((lastSegmentSkipped == segment) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) { + LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segment); + return; + } + + LogHelper.printDebug(() -> "Skipping segment: " + segment); + lastSegmentSkipped = segment; + lastSegmentSkippedTime = now; + segmentCurrentlyPlaying = null; + scheduledHideSegment = null; // if a scheduled has not run yet + scheduledUpcomingSegment = null; + SponsorBlockViewController.hideSkipButton(); + + final boolean seekSuccessful = VideoInformation.seekTo(segment.end); + if (!seekSuccessful) { + // can happen when switching videos and is normal + LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segment); + return; + } + + if (!userManuallySkipped) { + // check for any smaller embedded segments, and count those as autoskipped + final boolean showSkipToast = SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean(); + for (final SponsorSegment otherSegment : segmentsOfCurrentVideo) { + if (segment.end <= otherSegment.start) { + break; // no other segments can be contained + } + if (segment.containsSegment(otherSegment)) { // includes checking the segment against itself + otherSegment.didAutoSkipped = true; // skipped this segment as well + if (showSkipToast) { + showSkippedSegmentToast(otherSegment); + } + } + } + } + + if (segment.category == SegmentCategory.UNSUBMITTED) { + // skipped segment was a preview of unsubmitted segment + // remove the segment from the UI view + SponsorBlockUtils.setNewSponsorSegmentPreviewed(); + SponsorSegment[] newSegments = new SponsorSegment[segmentsOfCurrentVideo.length - 1]; + int i = 0; + for (SponsorSegment sponsorSegment : segmentsOfCurrentVideo) { + if (sponsorSegment != segment) + newSegments[i++] = sponsorSegment; + } + setSegmentsOfCurrentVideo(newSegments); + } else { + SponsorBlockUtils.sendViewRequestAsync(segment); + } + } catch (Exception ex) { + LogHelper.printException(() -> "skipSegment failure", ex); + } + } + + + private static int toastNumberOfSegmentsSkipped; + @Nullable + private static SponsorSegment toastSegmentSkipped; + + private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) { + ReVancedUtils.verifyOnMainThread(); + toastNumberOfSegmentsSkipped++; + if (toastNumberOfSegmentsSkipped > 1) { + return; // toast already scheduled + } + toastSegmentSkipped = segment; + + final long delayToToastMilliseconds = 200; // also the maximum time between skips to be considered skipping multiple segments + ReVancedUtils.runOnMainThreadDelayed(() -> { + try { + if (toastSegmentSkipped == null) { // video was changed just after skipping segment + LogHelper.printDebug(() -> "Ignoring old scheduled show toast"); + return; + } + ReVancedUtils.showToastShort(toastNumberOfSegmentsSkipped == 1 + ? toastSegmentSkipped.getSkippedToastText() + : str("sb_skipped_multiple_segments")); + } catch (Exception ex) { + LogHelper.printException(() -> "showSkippedSegmentToast failure", ex); + } finally { + toastNumberOfSegmentsSkipped = 0; + toastSegmentSkipped = null; + } + }, delayToToastMilliseconds); + } + + public static void onSkipSponsorClicked() { + if (segmentCurrentlyPlaying != null) { + skipSegment(segmentCurrentlyPlaying, true); + } else { + SponsorBlockViewController.hideSkipButton(); + LogHelper.printException(() -> "error: segment not available to skip"); // should never happen + } + } + + /** + * Injection point + */ + public static void setSponsorBarAbsoluteLeft(final Rect rect) { + setSponsorBarAbsoluteLeft(rect.left); + } + + public static void setSponsorBarAbsoluteLeft(final float left) { + if (sponsorBarLeft != left) { + LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteLeft: left=%.2f", left)); + sponsorBarLeft = left; + } + } + + /** + * Injection point + */ + public static void setSponsorBarRect(final Object self) { + try { + Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect"); + field.setAccessible(true); + Rect rect = (Rect) field.get(self); + if (rect == null) { + LogHelper.printException(() -> "Could not find sponsorblock rect"); + } else { + setSponsorBarAbsoluteLeft(rect.left); + setSponsorBarAbsoluteRight(rect.right); + } + } catch (Exception ex) { + LogHelper.printException(() -> "setSponsorBarRect failure", ex); + } + } + + /** + * Injection point + */ + public static void setSponsorBarAbsoluteRight(final Rect rect) { + setSponsorBarAbsoluteRight(rect.right); + } + + public static void setSponsorBarAbsoluteRight(final float right) { + if (sponsorBarRight != right) { + LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteRight: right=%.2f", right)); + sponsorBarRight = right; + } + } + + /** + * Injection point + */ + public static void setSponsorBarThickness(final int thickness) { + try { + setSponsorBarThickness((float) thickness); + } catch (Exception ex) { + LogHelper.printException(() -> "setSponsorBarThickness failure", ex); + } + } + + public static void setSponsorBarThickness(final float thickness) { + if (sponsorBarThickness != thickness) { + LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness)); + sponsorBarThickness = thickness; + } + } + + /** + * Injection point + */ + public static String appendTimeWithoutSegments(String totalTime) { + try { + if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() + && !TextUtils.isEmpty(totalTime) && !TextUtils.isEmpty(timeWithoutSegments)) { + return totalTime + timeWithoutSegments; + } + } catch (Exception ex) { + LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex); + } + + return totalTime; + } + + private static void calculateTimeWithoutSegments() { + final long currentVideoLength = VideoInformation.getCurrentVideoLength(); + if (!SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0 + || segmentsOfCurrentVideo == null || segmentsOfCurrentVideo.length == 0) { + timeWithoutSegments = null; + return; + } + + long timeWithoutSegmentsValue = currentVideoLength + 500; // YouTube:tm: + for (SponsorSegment segment : segmentsOfCurrentVideo) { + timeWithoutSegmentsValue -= segment.length(); + } + final long hours = timeWithoutSegmentsValue / 3600000; + final long minutes = (timeWithoutSegmentsValue / 60000) % 60; + final long seconds = (timeWithoutSegmentsValue / 1000) % 60; + if (hours > 0) { + timeWithoutSegments = String.format("\u2009(%d:%02d:%02d)", hours, minutes, seconds); + } else { + timeWithoutSegments = String.format("\u2009(%d:%02d)", minutes, seconds); + } + } + + /** + * Injection point + */ + public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { + try { + if (sponsorBarThickness < 0.1) return; + if (segmentsOfCurrentVideo == null) return; + final long currentVideoLength = VideoInformation.getCurrentVideoLength(); + if (currentVideoLength <= 0) return; + + final float thicknessDiv2 = sponsorBarThickness / 2; + final float top = posY - thicknessDiv2; + final float bottom = posY + thicknessDiv2; + final float absoluteLeft = sponsorBarLeft; + final float absoluteRight = sponsorBarRight; + + final float tmp1 = (1f / currentVideoLength) * (absoluteRight - absoluteLeft); + for (SponsorSegment segment : segmentsOfCurrentVideo) { + float left = segment.start * tmp1 + absoluteLeft; + float right = segment.end * tmp1 + absoluteLeft; + canvas.drawRect(left, top, right, bottom, segment.category.paint); + } + } catch (Exception ex) { + LogHelper.printException(() -> "drawSponsorTimeBars failure", ex); + } + } + +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ShieldButton.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ShieldButton.java deleted file mode 100644 index 4c5a55c3..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ShieldButton.java +++ /dev/null @@ -1,120 +0,0 @@ -package app.revanced.integrations.sponsorblock; - -import android.content.Context; - -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ImageView; -import android.widget.RelativeLayout; - -import java.lang.ref.WeakReference; - -import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength; -import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -public class ShieldButton { - static RelativeLayout _youtubeControlsLayout; - static WeakReference _shieldBtn = new WeakReference<>(null); - static int fadeDurationFast; - static int fadeDurationScheduled; - static Animation fadeIn; - static Animation fadeOut; - static boolean isShowing; - - public static void initialize(Object viewStub) { - try { - LogHelper.printDebug(() -> "initializing shield button"); - - _youtubeControlsLayout = (RelativeLayout) viewStub; - - ImageView imageView = (ImageView) _youtubeControlsLayout - .findViewById(getIdentifier("sponsorblock_button", "id")); - - if (imageView == null) { - LogHelper.printDebug(() -> "Couldn't find imageView with \"sponsorblock_button\""); - } - if (imageView == null) return; - imageView.setOnClickListener(SponsorBlockUtils.sponsorBlockBtnListener); - _shieldBtn = new WeakReference<>(imageView); - - // Animations - fadeDurationFast = getInteger("fade_duration_fast"); - fadeDurationScheduled = getInteger("fade_duration_scheduled"); - fadeIn = getAnimation("fade_in"); - fadeIn.setDuration(fadeDurationFast); - fadeOut = getAnimation("fade_out"); - fadeOut.setDuration(fadeDurationScheduled); - isShowing = true; - changeVisibilityImmediate(false); - } catch (Exception ex) { - LogHelper.printException(() -> "Unable to set RelativeLayout", ex); - } - } - - public static void changeVisibilityImmediate(boolean visible) { - changeVisibility(visible, true); - } - - public static void changeVisibilityNegatedImmediate(boolean visible) { - changeVisibility(!visible, true); - } - - public static void changeVisibility(boolean visible) { - changeVisibility(visible, false); - } - - public static void changeVisibility(boolean visible, boolean immediate) { - try { - if (isShowing == visible) return; - isShowing = visible; - - ImageView iView = _shieldBtn.get(); - if (_youtubeControlsLayout == null || iView == null) return; - - if (visible && shouldBeShown()) { - if (getLastKnownVideoTime() >= getCurrentVideoLength()) { - return; - } - LogHelper.printDebug(() -> "Fading in"); - - iView.setVisibility(View.VISIBLE); - if (!immediate) - iView.startAnimation(fadeIn); - return; - } - - if (iView.getVisibility() == View.VISIBLE) { - LogHelper.printDebug(() -> "Fading out"); - if (!immediate) - iView.startAnimation(fadeOut); - iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE); - } - } catch (Exception ex) { - LogHelper.printException(() -> "changeVisibility failure", ex); - } - } - - static boolean shouldBeShown() { - return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean(); - } - - //region Helpers - private static int getIdentifier(String name, String defType) { - Context context = ReVancedUtils.getContext(); - return context.getResources().getIdentifier(name, defType, context.getPackageName()); - } - - private static int getInteger(String name) { - return ReVancedUtils.getContext().getResources().getInteger(getIdentifier(name, "integer")); - } - - private static Animation getAnimation(String name) { - return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), getIdentifier(name, "anim")); - } - //endregion -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SkipSegmentView.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SkipSegmentView.java deleted file mode 100644 index e5e651a1..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SkipSegmentView.java +++ /dev/null @@ -1,46 +0,0 @@ -package app.revanced.integrations.sponsorblock; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.DisplayMetrics; - -import android.widget.Toast; - -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.sponsorblock.objects.SponsorSegment; -import app.revanced.integrations.utils.ReVancedUtils; - -import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.hideSkipButton; -import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.showSkipButton; - -@SuppressLint({"RtlHardcoded", "SetTextI18n", "AppCompatCustomView"}) -public class SkipSegmentView { - - private static SponsorSegment lastNotifiedSegment; - - public static void show() { - showSkipButton(); - } - - public static void hide() { - hideSkipButton(); - } - - public static void notifySkipped(SponsorSegment segment) { - if (segment == lastNotifiedSegment) { - LogHelper.printDebug(() -> "notifySkipped; segment == lastNotifiedSegment"); - return; - } - lastNotifiedSegment = segment; - String skipMessage = segment.category.skipMessage.toString(); - Context context = ReVancedUtils.getContext(); - LogHelper.printDebug(() -> String.format("notifySkipped; message=%s", skipMessage)); - - if (context != null) - Toast.makeText(context, skipMessage, Toast.LENGTH_SHORT).show(); - } - - public static float convertDpToPixel(float dp, Context context) { - return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java index de65f89b..22923d55 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java @@ -1,202 +1,195 @@ package app.revanced.integrations.sponsorblock; -import static app.revanced.integrations.sponsorblock.StringRef.sf; +import static app.revanced.integrations.utils.StringRef.str; -import android.app.Activity; import android.content.SharedPreferences; -import android.graphics.Color; -import android.graphics.Paint; -import android.text.Html; -import android.text.TextUtils; +import android.util.Patterns; +import androidx.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; +import app.revanced.integrations.sponsorblock.objects.SegmentCategory; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.SharedPrefHelper; public class SponsorBlockSettings { - public static final String CATEGORY_COLOR_SUFFIX = "_color"; - public static final SegmentBehaviour DefaultBehaviour = SegmentBehaviour.IGNORE; - public static String sponsorBlockUrlCategories = "[]"; + public static void importSettings(@NonNull String json) { + ReVancedUtils.verifyOnMainThread(); + try { + JSONObject settingsJson = new JSONObject(json); + JSONObject barTypesObject = settingsJson.getJSONObject("barTypes"); + JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections"); - public static void update(Activity _activity) { - SharedPreferences preferences = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK); + for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { + // clear existing behavior, as browser plugin exports no value for ignored categories + category.behaviour = CategoryBehaviour.IGNORE; + JSONObject categoryObject = barTypesObject.getJSONObject(category.key); + category.setColor(categoryObject.getString("color")); + } - if (!SettingsEnum.SB_ENABLED.getBoolean()) { - SkipSegmentView.hide(); - NewSegmentHelperLayout.hide(); - SponsorBlockUtils.hideShieldButton(); - SponsorBlockUtils.hideVoteButton(); - PlayerController.sponsorSegmentsOfCurrentVideo = null; - } else { /*isAddNewSegmentEnabled*/ - SponsorBlockUtils.showShieldButton(); - } + for (int i = 0; i < categorySelectionsArray.length(); i++) { + JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i); - if (!SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean()) { - NewSegmentHelperLayout.hide(); - SponsorBlockUtils.hideShieldButton(); - } else { - SponsorBlockUtils.showShieldButton(); - } + String categoryKey = categorySelectionObject.getString("name"); + SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey); + if (category == null) { + continue; // unsupported category, ignore + } - - if (!SettingsEnum.SB_VOTING_ENABLED.getBoolean()) - SponsorBlockUtils.hideVoteButton(); - else - SponsorBlockUtils.showVoteButton(); - - SegmentBehaviour[] possibleBehaviours = SegmentBehaviour.values(); - final ArrayList enabledCategories = new ArrayList<>(possibleBehaviours.length); - for (SegmentInfo segment : SegmentInfo.values()) { - String categoryColor = preferences.getString(segment.key + CATEGORY_COLOR_SUFFIX, SponsorBlockUtils.formatColorString(segment.defaultColor)); - segment.setColor(Color.parseColor(categoryColor)); - - SegmentBehaviour behaviour = null; - String value = preferences.getString(segment.key, null); - if (value != null) { - for (SegmentBehaviour possibleBehaviour : possibleBehaviours) { - if (possibleBehaviour.key.equals(value)) { - behaviour = possibleBehaviour; - break; - } + final int desktopKey = categorySelectionObject.getInt("option"); + CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKey(desktopKey); + if (behaviour != null) { + category.behaviour = behaviour; + } else { + LogHelper.printException(() -> "Unknown segment category behavior key: " + desktopKey); } } - if (behaviour != null) { - segment.behaviour = behaviour; - } else { - behaviour = segment.behaviour; + SegmentCategory.updateEnabledCategories(); + + SharedPreferences.Editor editor = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK).edit(); + for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { + category.save(editor); + } + editor.apply(); + + String userID = settingsJson.getString("userID"); + if (!isValidSBUserId(userID)) { + throw new IllegalArgumentException("userId is blank"); + } + SettingsEnum.SB_UUID.saveValue(userID); + + SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip")); + SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice")); + SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(settingsJson.getBoolean("trackViewCount")); + + String serverAddress = settingsJson.getString("serverAddress"); + if (!isValidSBServerAddress(serverAddress)) { + throw new IllegalArgumentException(str("sb_api_url_invalid")); + } + SettingsEnum.SB_API_URL.saveValue(serverAddress); + + SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips")); + final float minDuration = (float)settingsJson.getDouble("minDuration"); + if (minDuration < 0) { + throw new IllegalArgumentException("invalid minDuration: " + minDuration); + } + SettingsEnum.SB_MIN_DURATION.saveValue(minDuration); + + try { + int skipCount = settingsJson.getInt("skipCount"); + if (skipCount < 0) { + throw new IllegalArgumentException("invalid skipCount: " + skipCount); + } + SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(skipCount); + + final double minutesSaved = settingsJson.getDouble("minutesSaved"); + if (minutesSaved < 0) { + throw new IllegalArgumentException("invalid minutesSaved: " + minutesSaved); + } + SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue((long)(minutesSaved * 60 * 1000)); + } catch (JSONException ex) { + // ignore. values were not exported in prior versions of ReVanced } - if (behaviour.showOnTimeBar && segment != SegmentInfo.UNSUBMITTED) - enabledCategories.add(segment.key); + ReVancedUtils.showToastLong(str("sb_settings_import_successful")); + } catch (Exception ex) { + LogHelper.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast + ReVancedUtils.showToastLong(str("sb_settings_import_failed", ex.getMessage())); } + } - //"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]"; - if (enabledCategories.isEmpty()) - sponsorBlockUrlCategories = "[]"; - else - sponsorBlockUrlCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]"; + @NonNull + public static String exportSettings() { + ReVancedUtils.verifyOnMainThread(); + try { + LogHelper.printDebug(() -> "Creating SponsorBlock export settings string"); + JSONObject json = new JSONObject(); + + JSONObject barTypesObject = new JSONObject(); // categories' colors + JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior + + SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted(); + for (SegmentCategory category : categories) { + JSONObject categoryObject = new JSONObject(); + String categoryKey = category.key; + categoryObject.put("color", category.colorString()); + barTypesObject.put(categoryKey, categoryObject); + + JSONObject behaviorObject = new JSONObject(); + behaviorObject.put("name", categoryKey); + behaviorObject.put("option", category.behaviour.desktopKey); + categorySelectionsArray.put(behaviorObject); + } + json.put("userID", SettingsEnum.SB_UUID.getString()); + json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean()); + json.put("serverAddress", SettingsEnum.SB_API_URL.getString()); + json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean()); + json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()); + json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat()); + json.put("trackViewCount", SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()); + json.put("skipCount", SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt()); + json.put("minutesSaved", SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / (60f * 1000)); + + json.put("categorySelections", categorySelectionsArray); + json.put("barTypes", barTypesObject); + + return json.toString(2); + } catch (Exception ex) { + LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast + ReVancedUtils.showToastLong(str("sb_settings_export_failed")); + return ""; + } + } + + public static boolean isValidSBUserId(@NonNull String userId) { + return !userId.isEmpty(); + } + + /** + * A non comprehensive check if a SB api server address is valid. + */ + public static boolean isValidSBServerAddress(@NonNull String serverAddress) { + if (!Patterns.WEB_URL.matcher(serverAddress).matches()) { + return false; + } + // Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/" + // Could use Patterns.compile, but this is simpler + final int lastDotIndex = serverAddress.lastIndexOf('.'); + if (lastDotIndex != -1 && serverAddress.substring(lastDotIndex).contains("/")) { + return false; + } + // Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)" + // but that should not be done on the main thread. + // Instead, assume the domain exists and the user knows what they're doing. + return true; + } + + private static boolean initialized; + + public static void initialize() { + if (initialized) { + return; + } + initialized = true; String uuid = SettingsEnum.SB_UUID.getString(); - if (uuid == null || uuid.length() == 0) { + if (uuid == null || uuid.isEmpty()) { uuid = (UUID.randomUUID().toString() + UUID.randomUUID().toString() + UUID.randomUUID().toString()) .replace("-", ""); SettingsEnum.SB_UUID.saveValue(uuid); } - } - public enum SegmentBehaviour { - SKIP_AUTOMATICALLY_ONCE("skip-once", 3, sf("skip_automatically_once"), true, true), - SKIP_AUTOMATICALLY("skip", 2, sf("skip_automatically"), true, true), - MANUAL_SKIP("manual-skip", 1, sf("skip_showbutton"), false, true), - IGNORE("ignore", -1, sf("skip_ignore"), false, false); - - public final String key; - public final int desktopKey; - public final StringRef name; - public final boolean skip; - public final boolean showOnTimeBar; - - SegmentBehaviour(String key, - int desktopKey, - StringRef name, - boolean skip, - boolean showOnTimeBar) { - this.key = key; - this.desktopKey = desktopKey; - this.name = name; - this.skip = skip; - this.showOnTimeBar = showOnTimeBar; - } - - public static SegmentBehaviour byDesktopKey(int desktopKey) { - for (SegmentBehaviour behaviour : values()) { - if (behaviour.desktopKey == desktopKey) { - return behaviour; - } - } - return null; - } - } - - public enum SegmentInfo { - SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFF00d400), - INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFF00ffff), - OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFF0202ed), - INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFcc00ff), - SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFffff00), - MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFFff9900), - PREVIEW("preview", sf("segments_preview"), sf("skipped_preview"), sf("segments_preview_sum"), DefaultBehaviour, 0xFF008fd6), - FILLER("filler", sf("segments_filler"), sf("skipped_filler"), sf("segments_filler_sum"), DefaultBehaviour, 0xFF7300FF), - UNSUBMITTED("unsubmitted", StringRef.empty, sf("skipped_unsubmitted"), StringRef.empty, SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFFFFFFF); - - private static final SegmentInfo[] mValuesWithoutUnsubmitted = new SegmentInfo[]{ - SPONSOR, - INTRO, - OUTRO, - INTERACTION, - SELF_PROMO, - MUSIC_OFFTOPIC, - PREVIEW, - FILLER - }; - private static final Map mValuesMap = new HashMap<>(values().length); - - static { - for (SegmentInfo value : valuesWithoutUnsubmitted()) - mValuesMap.put(value.key, value); - } - - public final String key; - public final StringRef title; - public final StringRef skipMessage; - public final StringRef description; - public final Paint paint; - public final int defaultColor; - public int color; - public SegmentBehaviour behaviour; - - SegmentInfo(String key, - StringRef title, - StringRef skipMessage, - StringRef description, - SegmentBehaviour behaviour, - int defaultColor) { - - this.key = key; - this.title = title; - this.skipMessage = skipMessage; - this.description = description; - this.behaviour = behaviour; - this.defaultColor = defaultColor; - this.color = defaultColor; - this.paint = new Paint(); - } - - public static SegmentInfo[] valuesWithoutUnsubmitted() { - return mValuesWithoutUnsubmitted; - } - - public static SegmentInfo byCategoryKey(String key) { - return mValuesMap.get(key); - } - - public void setColor(int color) { - color = color & 0xFFFFFF; - this.color = color; - paint.setColor(color); - paint.setAlpha(255); - } - - public CharSequence getTitleWithDot() { - return Html.fromHtml(String.format(" %s", color, title)); - } + SegmentCategory.loadFromPreferences(); } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java index 0491b382..d5b0b6f6 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java @@ -1,41 +1,20 @@ package app.revanced.integrations.sponsorblock; -import static android.text.Html.fromHtml; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoId; -import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength; -import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime; -import static app.revanced.integrations.sponsorblock.PlayerController.sponsorSegmentsOfCurrentVideo; -import static app.revanced.integrations.settingsmenu.SponsorBlockSettingsFragment.FORMATTER; -import static app.revanced.integrations.settingsmenu.SponsorBlockSettingsFragment.SAVED_TEMPLATE; -import static app.revanced.integrations.sponsorblock.StringRef.str; -import static app.revanced.integrations.sponsorblock.requests.SBRequester.voteForSegment; +import static app.revanced.integrations.utils.StringRef.str; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceCategory; import android.text.Html; -import android.text.TextUtils; - -import android.view.View; import android.widget.EditText; -import android.widget.ImageView; -import android.widget.Toast; -import org.json.JSONArray; -import org.json.JSONObject; +import androidx.annotation.NonNull; import java.lang.ref.WeakReference; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -43,599 +22,414 @@ import java.util.List; import java.util.Objects; import java.util.TimeZone; +import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.sponsorblock.player.PlayerType; +import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; +import app.revanced.integrations.sponsorblock.objects.SegmentCategory; +import app.revanced.integrations.sponsorblock.objects.SponsorSegment; +import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote; +import app.revanced.integrations.sponsorblock.requests.SBRequester; +import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; -import app.revanced.integrations.sponsorblock.objects.SponsorSegment; -import app.revanced.integrations.sponsorblock.objects.UserStats; -import app.revanced.integrations.sponsorblock.requests.SBRequester; -public abstract class SponsorBlockUtils { - public static final String DATE_FORMAT = "HH:mm:ss.SSS"; +/** + * Not thread safe. All fields/methods must be accessed from the main thread. + */ +public class SponsorBlockUtils { + private static final String MANUAL_EDIT_TIME_FORMAT = "HH:mm:ss.SSS"; @SuppressLint("SimpleDateFormat") - public static final SimpleDateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT); - public static boolean videoHasSegments = false; - public static String timeWithoutSegments = ""; - private static final int sponsorBtnId = 1234; + private static final SimpleDateFormat manualEditTimeFormatter = new SimpleDateFormat(MANUAL_EDIT_TIME_FORMAT); + @SuppressLint("SimpleDateFormat") + private static final SimpleDateFormat voteSegmentTimeFormatter = new SimpleDateFormat(); + static { + TimeZone utc = TimeZone.getTimeZone("UTC"); + manualEditTimeFormatter.setTimeZone(utc); + voteSegmentTimeFormatter.setTimeZone(utc); + } private static final String LOCKED_COLOR = "#FFC83D"; - public static final View.OnClickListener sponsorBlockBtnListener = v -> { - LogHelper.printDebug(() -> "Shield button clicked"); - NewSegmentHelperLayout.toggle(); - }; - public static final View.OnClickListener voteButtonListener = v -> { - LogHelper.printDebug(() -> "Vote button clicked"); - SponsorBlockUtils.onVotingClicked(v.getContext()); - }; - private static int shareBtnId = -1; + private static long newSponsorSegmentDialogShownMillis; private static long newSponsorSegmentStartMillis = -1; private static long newSponsorSegmentEndMillis = -1; + private static boolean newSponsorSegmentPreviewed; private static final DialogInterface.OnClickListener newSponsorSegmentDialogListener = new DialogInterface.OnClickListener() { - @SuppressLint("DefaultLocale") @Override public void onClick(DialogInterface dialog, int which) { - Context context = ((AlertDialog) dialog).getContext(); switch (which) { case DialogInterface.BUTTON_NEGATIVE: // start newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis; - Toast.makeText(context.getApplicationContext(), str("new_segment_time_start_set"), Toast.LENGTH_LONG).show(); break; case DialogInterface.BUTTON_POSITIVE: // end newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis; - Toast.makeText(context.getApplicationContext(), str("new_segment_time_end_set"), Toast.LENGTH_SHORT).show(); break; } dialog.dismiss(); } }; - private static SponsorBlockSettings.SegmentInfo newSponsorBlockSegmentType; + private static SegmentCategory newUserCreatedSegmentCategory; private static final DialogInterface.OnClickListener segmentTypeListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted()[which]; - boolean enableButton; - if (!segmentType.behaviour.showOnTimeBar) { - Toast.makeText( - ((AlertDialog) dialog).getContext().getApplicationContext(), - str("new_segment_disabled_category"), - Toast.LENGTH_SHORT).show(); - enableButton = false; - } else { - Toast.makeText( - ((AlertDialog) dialog).getContext().getApplicationContext(), - segmentType.description.toString(), - Toast.LENGTH_SHORT).show(); - newSponsorBlockSegmentType = segmentType; - enableButton = true; - } + try { + SegmentCategory category = SegmentCategory.valuesWithoutUnsubmitted()[which]; + boolean enableButton; + if (category.behaviour == CategoryBehaviour.IGNORE) { + ReVancedUtils.showToastLong(str("sb_new_segment_disabled_category")); + enableButton = false; + } else { + newUserCreatedSegmentCategory = category; + enableButton = true; + } - ((AlertDialog) dialog) - .getButton(DialogInterface.BUTTON_POSITIVE) - .setEnabled(enableButton); + ((AlertDialog) dialog) + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(enableButton); + } catch (Exception ex) { + LogHelper.printException(() -> "segmentTypeListener failure", ex); + } } }; private static final DialogInterface.OnClickListener segmentReadyDialogButtonListener = new DialogInterface.OnClickListener() { - @SuppressLint("DefaultLocale") @Override public void onClick(DialogInterface dialog, int which) { - NewSegmentHelperLayout.hide(); - Context context = ((AlertDialog) dialog).getContext(); - dialog.dismiss(); + try { + SponsorBlockViewController.hideNewSegmentLayout(); + Context context = ((AlertDialog) dialog).getContext(); + dialog.dismiss(); - SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted(); - CharSequence[] titles = new CharSequence[values.length]; - for (int i = 0; i < values.length; i++) { -// titles[i] = values[i].title; - titles[i] = values[i].getTitleWithDot(); + SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted(); + CharSequence[] titles = new CharSequence[categories.length]; + for (int i = 0, length = categories.length; i < length; i++) { + titles[i] = categories[i].getTitleWithColorDot(); + } + + newUserCreatedSegmentCategory = null; + new AlertDialog.Builder(context) + .setTitle(str("sb_new_segment_choose_category")) + .setSingleChoiceItems(titles, -1, segmentTypeListener) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener) + .show() + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(false); + } catch (Exception ex) { + LogHelper.printException(() -> "segmentReadyDialogButtonListener failure", ex); } - - newSponsorBlockSegmentType = null; - new AlertDialog.Builder(context) - .setTitle(str("new_segment_choose_category")) - .setSingleChoiceItems(titles, -1, segmentTypeListener) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener) - .show() - .getButton(DialogInterface.BUTTON_POSITIVE) - .setEnabled(false); } }; - private static WeakReference appContext = new WeakReference<>(null); - private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = new DialogInterface.OnClickListener() { - @SuppressLint("DefaultLocale") - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - Context context = ((AlertDialog) dialog).getContext().getApplicationContext(); - Toast.makeText(context, str("submit_started"), Toast.LENGTH_SHORT).show(); - - appContext = new WeakReference<>(context); - ReVancedUtils.runOnBackgroundThread(submitRunnable); - } + private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = (dialog, which) -> { + dialog.dismiss(); + submitNewSegment(); }; - public static String messageToToast = ""; private static final EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener(); private static final DialogInterface.OnClickListener editByHandDialogListener = (dialog, which) -> { - Context context = ((AlertDialog) dialog).getContext(); + try { + Context context = ((AlertDialog) dialog).getContext(); - final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which; + final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which; - final EditText textView = new EditText(context); - textView.setHint(DATE_FORMAT); - if (isStart) { - if (newSponsorSegmentStartMillis >= 0) - textView.setText(dateFormatter.format(new Date(newSponsorSegmentStartMillis))); - } else { - if (newSponsorSegmentEndMillis >= 0) - textView.setText(dateFormatter.format(new Date(newSponsorSegmentEndMillis))); + final EditText textView = new EditText(context); + textView.setHint(MANUAL_EDIT_TIME_FORMAT); + if (isStart) { + if (newSponsorSegmentStartMillis >= 0) + textView.setText(manualEditTimeFormatter.format(new Date(newSponsorSegmentStartMillis))); + } else { + if (newSponsorSegmentEndMillis >= 0) + textView.setText(manualEditTimeFormatter.format(new Date(newSponsorSegmentEndMillis))); + } + + editByHandSaveDialogListener.settingStart = isStart; + editByHandSaveDialogListener.editText = new WeakReference<>(textView); + new AlertDialog.Builder(context) + .setTitle(str(isStart ? "sb_new_segment_time_start" : "sb_new_segment_time_end")) + .setView(textView) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("sb_new_segment_now"), editByHandSaveDialogListener) + .setPositiveButton(android.R.string.ok, editByHandSaveDialogListener) + .show(); + + dialog.dismiss(); + } catch (Exception ex) { + LogHelper.printException(() -> "editByHandDialogListener failure", ex); } - - editByHandSaveDialogListener.settingStart = isStart; - editByHandSaveDialogListener.editText = new WeakReference<>(textView); - new AlertDialog.Builder(context) - .setTitle(str(isStart ? "new_segment_time_start" : "new_segment_time_end")) - .setView(textView) - .setNegativeButton(android.R.string.cancel, null) - .setNeutralButton(str("new_segment_now"), editByHandSaveDialogListener) - .setPositiveButton(android.R.string.ok, editByHandSaveDialogListener) - .show(); - - dialog.dismiss(); - }; - private static final Runnable toastRunnable = () -> { - Context context = appContext.get(); - if (context != null && messageToToast != null) - Toast.makeText(context, messageToToast, Toast.LENGTH_LONG).show(); }; private static final DialogInterface.OnClickListener segmentVoteClickListener = (dialog, which) -> { - final Context context = ((AlertDialog) dialog).getContext(); - final SponsorSegment segment = sponsorSegmentsOfCurrentVideo[which]; - - final VoteOption[] voteOptions = VoteOption.values(); - CharSequence[] items = new CharSequence[voteOptions.length]; - - for (int i = 0; i < voteOptions.length; i++) { - VoteOption voteOption = voteOptions[i]; - String title = voteOption.title; - if (SettingsEnum.SB_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) { - items[i] = Html.fromHtml(String.format("%s", LOCKED_COLOR, title)); - } else { - items[i] = title; - } - } - - new AlertDialog.Builder(context) - .setItems(items, (dialog1, which1) -> { - appContext = new WeakReference<>(context.getApplicationContext()); - VoteOption voteOption = voteOptions[which1]; - switch (voteOption) { - case UPVOTE: - case DOWNVOTE: - voteForSegment(segment, voteOption, appContext.get()); - break; - case CATEGORY_CHANGE: - onNewCategorySelect(segment, context); - break; - } - }) - .show(); - }; - private static final Runnable submitRunnable = () -> { - messageToToast = null; - final String uuid = SettingsEnum.SB_UUID.getString(); - final long start = newSponsorSegmentStartMillis; - final long end = newSponsorSegmentEndMillis; - final String videoId = getCurrentVideoId(); - final SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockUtils.newSponsorBlockSegmentType; try { - if (start < 0 || end < 0 || start >= end || segmentType == null || videoId == null || uuid == null) { - LogHelper.printException(() -> "Unable to submit times, invalid parameters"); + final Context context = ((AlertDialog) dialog).getContext(); + SponsorSegment[] currentSegments = SegmentPlaybackController.getSegmentsOfCurrentVideo(); + if (currentSegments == null || currentSegments.length == 0) { + // should never be reached + LogHelper.printException(() -> "Segment is no longer available on the client"); return; } - SBRequester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable); - newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1; - } catch (Exception e) { - LogHelper.printException(() -> "Unable to submit segment", e); + SponsorSegment segment = currentSegments[which]; + + SegmentVote[] voteOptions = SegmentVote.values(); + CharSequence[] items = new CharSequence[voteOptions.length]; + + for (int i = 0; i < voteOptions.length; i++) { + SegmentVote voteOption = voteOptions[i]; + String title = voteOption.title.toString(); + if (SettingsEnum.SB_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) { + items[i] = Html.fromHtml(String.format("%s", LOCKED_COLOR, title)); + } else { + items[i] = title; + } + } + + new AlertDialog.Builder(context) + .setItems(items, (dialog1, which1) -> { + SegmentVote voteOption = voteOptions[which1]; + switch (voteOption) { + case UPVOTE: + case DOWNVOTE: + SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption); + break; + case CATEGORY_CHANGE: + onNewCategorySelect(segment, context); + break; + } + }) + .show(); + } catch (Exception ex) { + LogHelper.printException(() -> "onPreviewClicked failure", ex); } - - if (videoId != null) - PlayerController.executeDownloadSegments(videoId); }; - static { - dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); - } - private SponsorBlockUtils() { } - public static void showShieldButton() { - View i = ShieldButton._shieldBtn.get(); - if (i == null || !ShieldButton.shouldBeShown()) return; - i.setVisibility(VISIBLE); - i.bringToFront(); - i.requestLayout(); - i.invalidate(); + static void setNewSponsorSegmentPreviewed() { + newSponsorSegmentPreviewed = true; } - public static void hideShieldButton() { - View i = ShieldButton._shieldBtn.get(); - if (i != null) - i.setVisibility(GONE); + static void clearUnsubmittedSegmentTimes() { + newSponsorSegmentDialogShownMillis = 0; + newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1; + newSponsorSegmentPreviewed = false; } - public static void showVoteButton() { - View i = VotingButton._votingButton.get(); - if (i == null || !VotingButton.shouldBeShown()) return; - i.setVisibility(VISIBLE); - i.bringToFront(); - i.requestLayout(); - i.invalidate(); + private static void submitNewSegment() { + try { + ReVancedUtils.verifyOnMainThread(); + final String uuid = SettingsEnum.SB_UUID.getString(); + final long start = newSponsorSegmentStartMillis; + final long end = newSponsorSegmentEndMillis; + final String videoId = SegmentPlaybackController.getCurrentVideoId(); + final long videoLength = VideoInformation.getCurrentVideoLength(); + final SegmentCategory segmentCategory = newUserCreatedSegmentCategory; + if (start < 0 || end < 0 || start >= end || videoLength <= 0 || segmentCategory == null || videoId == null || uuid == null) { + LogHelper.printException(() -> "Unable to submit times, invalid parameters"); + return; + } + clearUnsubmittedSegmentTimes(); + ReVancedUtils.runOnBackgroundThread(() -> { + SBRequester.submitSegments(uuid, videoId, segmentCategory.key, start, end, videoLength); + SegmentPlaybackController.executeDownloadSegments(videoId); + }); + } catch (Exception e) { + LogHelper.printException(() -> "Unable to submit segment", e); + } } - public static void hideVoteButton() { - View i = VotingButton._votingButton.get(); - if (i != null) - i.setVisibility(GONE); - } + public static void onMarkLocationClicked() { + try { + ReVancedUtils.verifyOnMainThread(); + newSponsorSegmentDialogShownMillis = VideoInformation.getVideoTime(); - @SuppressLint("DefaultLocale") - public static void onMarkLocationClicked(Context context) { - newSponsorSegmentDialogShownMillis = PlayerController.getLastKnownVideoTime(); - - new AlertDialog.Builder(context) - .setTitle(str("new_segment_title")) - .setMessage(str("new_segment_mark_time_as_question", - newSponsorSegmentDialogShownMillis / 60000, - newSponsorSegmentDialogShownMillis / 1000 % 60, - newSponsorSegmentDialogShownMillis % 1000)) - .setNeutralButton(android.R.string.cancel, null) - .setNegativeButton(str("new_segment_mark_start"), newSponsorSegmentDialogListener) - .setPositiveButton(str("new_segment_mark_end"), newSponsorSegmentDialogListener) - .show(); - } - - @SuppressLint("DefaultLocale") - public static void onPublishClicked(Context context) { - if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { - long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000; - long start = (newSponsorSegmentStartMillis) / 1000; - long end = (newSponsorSegmentEndMillis) / 1000; - new AlertDialog.Builder(context) - .setTitle(str("new_segment_confirm_title")) - .setMessage(str("new_segment_confirm_content", - start / 60, start % 60, - end / 60, end % 60, - length / 60, length % 60)) - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener) + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setTitle(str("sb_new_segment_title")) + .setMessage(str("sb_new_segment_mark_time_as_question", + newSponsorSegmentDialogShownMillis / 60000, + newSponsorSegmentDialogShownMillis / 1000 % 60, + newSponsorSegmentDialogShownMillis % 1000)) + .setNeutralButton(android.R.string.cancel, null) + .setNegativeButton(str("sb_new_segment_mark_start"), newSponsorSegmentDialogListener) + .setPositiveButton(str("sb_new_segment_mark_end"), newSponsorSegmentDialogListener) .show(); - } else { - Toast.makeText(context, str("new_segment_mark_locations_first"), Toast.LENGTH_SHORT).show(); + } catch (Exception ex) { + LogHelper.printException(() -> "onMarkLocationClicked failure", ex); } } - public static void onVotingClicked(final Context context) { - if (sponsorSegmentsOfCurrentVideo == null || sponsorSegmentsOfCurrentVideo.length == 0) { - Toast.makeText(context.getApplicationContext(), str("vote_no_segments"), Toast.LENGTH_SHORT).show(); - return; - } - int segmentAmount = sponsorSegmentsOfCurrentVideo.length; - List titles = new ArrayList<>(segmentAmount); // I've replaced an array with a list to prevent null elements in the array as unsubmitted segments get filtered out - for (int i = 0; i < segmentAmount; i++) { - SponsorSegment segment = sponsorSegmentsOfCurrentVideo[i]; - if (segment.category == SponsorBlockSettings.SegmentInfo.UNSUBMITTED) { - continue; - } - - String start = dateFormatter.format(new Date(segment.start)); - String end = dateFormatter.format(new Date(segment.end)); - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.append(String.format(" %s
%s to %s", - segment.category.color, segment.category.title, start, end)); - if (i + 1 != segmentAmount) // prevents trailing new line after last segment - htmlBuilder.append("
"); - titles.add(Html.fromHtml(htmlBuilder.toString())); - } - - new AlertDialog.Builder(context) - .setItems(titles.toArray(new CharSequence[0]), segmentVoteClickListener) - .show(); - } - - private static void onNewCategorySelect(final SponsorSegment segment, Context context) { - final SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted(); - CharSequence[] titles = new CharSequence[values.length]; - for (int i = 0; i < values.length; i++) { - titles[i] = values[i].getTitleWithDot(); - } - - new AlertDialog.Builder(context) - .setTitle(str("new_segment_choose_category")) - .setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), values[which].key)) - .show(); - } - - @SuppressLint("DefaultLocale") - public static void onPreviewClicked(Context context) { - if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { -// Toast t = Toast.makeText(context, "Preview", Toast.LENGTH_SHORT); -// t.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.TOP, t.getXOffset(), t.getYOffset()); -// t.show(); - PlayerController.skipToMillisecond(newSponsorSegmentStartMillis - 3000); - final SponsorSegment[] original = PlayerController.sponsorSegmentsOfCurrentVideo; - final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1); - - segments[segments.length - 1] = new SponsorSegment(newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, - SponsorBlockSettings.SegmentInfo.UNSUBMITTED, null, false); - - Arrays.sort(segments); - sponsorSegmentsOfCurrentVideo = segments; - } else { - Toast.makeText(context, str("new_segment_mark_locations_first"), Toast.LENGTH_SHORT).show(); - } - } - - @SuppressLint("DefaultLocale") - public static void onEditByHandClicked(Context context) { - new AlertDialog.Builder(context) - .setTitle(str("new_segment_edit_by_hand_title")) - .setMessage(str("new_segment_edit_by_hand_content")) - .setNeutralButton(android.R.string.cancel, null) - .setNegativeButton(str("new_segment_mark_start"), editByHandDialogListener) - .setPositiveButton(str("new_segment_mark_end"), editByHandDialogListener) - .show(); - } - - public static void notifyShareBtnVisibilityChanged(View v) { - if (v.getId() != shareBtnId || !/*SponsorBlockSettings.isAddNewSegmentEnabled*/false) - return; -// if (VERBOSE) -// LogH(TAG, "VISIBILITY CHANGED of view " + v); - ImageView sponsorBtn = ShieldButton._shieldBtn.get(); - if (sponsorBtn != null) { - sponsorBtn.setVisibility(v.getVisibility()); - } - } - - public static String appendTimeWithoutSegments(String totalTime) { + public static void onPublishClicked() { try { - if (videoHasSegments && (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()) && !TextUtils.isEmpty(totalTime) && getCurrentVideoLength() > 1) { - if (timeWithoutSegments.isEmpty()) { - timeWithoutSegments = getTimeWithoutSegments(sponsorSegmentsOfCurrentVideo); - } - return totalTime + timeWithoutSegments; + ReVancedUtils.verifyOnMainThread(); + if (!newSponsorSegmentPreviewed) { + ReVancedUtils.showToastLong(str("sb_new_segment_preview_segment_first")); + } else if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { + long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000; + long start = (newSponsorSegmentStartMillis) / 1000; + long end = (newSponsorSegmentEndMillis) / 1000; + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setTitle(str("sb_new_segment_confirm_title")) + .setMessage(str("sb_new_segment_confirm_content", + start / 60, start % 60, + end / 60, end % 60, + length / 60, length % 60)) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener) + .show(); + } else { + ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first")); } } catch (Exception ex) { - LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex); + LogHelper.printException(() -> "onPublishClicked failure", ex); } - - return totalTime; } - public static String getTimeWithoutSegments(SponsorSegment[] sponsorSegmentsOfCurrentVideo) { - long currentVideoLength = getCurrentVideoLength(); - if (!(SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()) || sponsorSegmentsOfCurrentVideo == null || currentVideoLength <= 1) { - return ""; - } - long timeWithoutSegments = currentVideoLength + 500; // YouTube:tm: - for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) { - timeWithoutSegments -= segment.end - segment.start; - } - long hours = timeWithoutSegments / 3600000; - long minutes = (timeWithoutSegments / 60000) % 60; - long seconds = (timeWithoutSegments / 1000) % 60; - String format = (hours > 0 ? "%d:%02" : "%") + "d:%02d"; // mmLul - String formatted = hours > 0 ? String.format(format, hours, minutes, seconds) : String.format(format, minutes, seconds); - return String.format(" (%s)", formatted); - } - - public static void playerTypeChanged(PlayerType playerType) { + public static void onVotingClicked(@NonNull Context context) { try { - if (videoHasSegments && (playerType == PlayerType.NONE)) { - PlayerController.setCurrentVideoId(null); - } - } catch (Exception ex) { - LogHelper.printException(() -> "Player type changed caused a crash.", ex); - } - } - - public static String formatColorString(int color) { - return String.format("#%06X", color); - } - - @SuppressWarnings("deprecation") - public static void addUserStats(PreferenceCategory category, Preference loadingPreference, UserStats stats) { - category.removePreference(loadingPreference); - - Context context = category.getContext(); - String minutesStr = str("minutes"); - - { - EditTextPreference preference = new EditTextPreference(context); - category.addPreference(preference); - String userName = stats.getUserName(); - preference.setTitle(fromHtml(str("stats_username", userName))); - preference.setSummary(str("stats_username_change")); - preference.setText(userName); - preference.setOnPreferenceChangeListener((preference1, newUsername) -> { - appContext = new WeakReference<>(context.getApplicationContext()); - SBRequester.setUsername((String) newUsername, preference, toastRunnable); - return false; - }); - } - - { - Preference preference = new Preference(context); - category.addPreference(preference); - String formatted = FORMATTER.format(stats.getSegmentCount()); - preference.setTitle(fromHtml(str("stats_submissions", formatted))); - preference.setSelectable(false); - } - - { - Preference preference = new Preference(context); - category.addPreference(preference); - String formatted = FORMATTER.format(stats.getViewCount()); - - double saved = stats.getMinutesSaved(); - int hoursSaved = (int) (saved / 60); - double minutesSaved = saved % 60; - String formattedSaved = String.format(SAVED_TEMPLATE, hoursSaved, minutesSaved, minutesStr); - - preference.setTitle(fromHtml(str("stats_saved", formatted))); - preference.setSummary(fromHtml(str("stats_saved_sum", formattedSaved))); - preference.setOnPreferenceClickListener(preference1 -> { - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse("https://sponsor.ajay.app/stats/")); - preference1.getContext().startActivity(i); - return false; - }); - } - - { - Preference preference = new Preference(context); - category.addPreference(preference); - String formatted = FORMATTER.format(SettingsEnum.SB_SKIPPED_SEGMENTS.getInt()); - - long hoursSaved = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() / 3600000; - double minutesSaved = (SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() / 60000d) % 60; - String formattedSaved = String.format(SAVED_TEMPLATE, hoursSaved, minutesSaved, minutesStr); - - preference.setTitle(fromHtml(str("stats_self_saved", formatted))); - preference.setSummary(fromHtml(str("stats_self_saved_sum", formattedSaved))); - preference.setSelectable(false); - } - } - - public static void importSettings(String json, Context context) { - try { - JSONObject settingsJson = new JSONObject(json); - - JSONObject barTypesObject = settingsJson.getJSONObject("barTypes"); - JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections"); - - - SharedPreferences.Editor editor = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK).edit(); - - SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted(); - for (SponsorBlockSettings.SegmentInfo category : categories) { - String categoryKey = category.key; - JSONObject categoryObject = barTypesObject.getJSONObject(categoryKey); - String color = categoryObject.getString("color"); - - editor.putString(categoryKey + SponsorBlockSettings.CATEGORY_COLOR_SUFFIX, color); - editor.putString(categoryKey, SponsorBlockSettings.SegmentBehaviour.IGNORE.key); + ReVancedUtils.verifyOnMainThread(); + SponsorSegment[] currentSegments = SegmentPlaybackController.getSegmentsOfCurrentVideo(); + if (currentSegments == null || currentSegments.length == 0) { + // button is hidden if no segments exist. + // But if prior video had segments, and current video does not, + // then the button persists until the overlay fades out (this is intentional, as abruptly hiding the button is jarring) + ReVancedUtils.showToastShort(str("sb_vote_no_segments")); + return; } - for (int i = 0; i < categorySelectionsArray.length(); i++) { - JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i); + // use same time formatting as shown in the video player + final long currentVideoLength = VideoInformation.getCurrentVideoLength(); + final String formatPattern; + if (currentVideoLength < (10 * 60 * 1000)) { + formatPattern = "m:ss"; // less than 10 minutes + } else if (currentVideoLength < (60 * 60 * 1000)) { + formatPattern = "mm:ss"; // less than 1 hour + } else if (currentVideoLength < (10 * 60 * 60 * 1000)) { + formatPattern = "H:mm:ss"; // less than 10 hours + } else { + formatPattern = "HH:mm:ss"; // why is this on YouTube + } + voteSegmentTimeFormatter.applyPattern(formatPattern); - String categoryKey = categorySelectionObject.getString("name"); - SponsorBlockSettings.SegmentInfo category = SponsorBlockSettings.SegmentInfo.byCategoryKey(categoryKey); - - if (category == null) { + final int numberOfSegments = currentSegments.length; + List titles = new ArrayList<>(numberOfSegments); + for (int i = 0; i < numberOfSegments; i++) { + SponsorSegment segment = currentSegments[i]; + if (segment.category == SegmentCategory.UNSUBMITTED) { continue; } - - int desktopKey = categorySelectionObject.getInt("option"); - SponsorBlockSettings.SegmentBehaviour behaviour = SponsorBlockSettings.SegmentBehaviour.byDesktopKey(desktopKey); - editor.putString(category.key, behaviour.key); + String start = voteSegmentTimeFormatter.format(new Date(segment.start)); + String end = voteSegmentTimeFormatter.format(new Date(segment.end)); + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(String.format(" %s
%s to %s", + segment.category.color, segment.category.title, start, end)); + if (i + 1 != numberOfSegments) // prevents trailing new line after last segment + htmlBuilder.append("
"); + titles.add(Html.fromHtml(htmlBuilder.toString())); } - SettingsEnum.SB_UUID.saveValue(settingsJson.getString("userID")); - SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip")); - SettingsEnum.SB_API_URL.saveValue(settingsJson.getString("serverAddress")); - SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice")); - SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips")); - SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(settingsJson.getString("minDuration"))); - SettingsEnum.SB_COUNT_SKIPS.saveValue(settingsJson.getBoolean("trackViewCount")); - - Toast.makeText(context, str("settings_import_successful"), Toast.LENGTH_SHORT).show(); + new AlertDialog.Builder(context) + .setItems(titles.toArray(new CharSequence[0]), segmentVoteClickListener) + .show(); } catch (Exception ex) { - LogHelper.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast - Toast.makeText(context, str("settings_import_failed"), Toast.LENGTH_SHORT).show(); + LogHelper.printException(() -> "onVotingClicked failure", ex); } } - public static String exportSettings(Context context) { + private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) { try { - JSONObject json = new JSONObject(); - - JSONObject barTypesObject = new JSONObject(); // categories' colors - JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior - - SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted(); - for (SponsorBlockSettings.SegmentInfo category : categories) { - JSONObject categoryObject = new JSONObject(); - String categoryKey = category.key; - categoryObject.put("color", formatColorString(category.color)); - barTypesObject.put(categoryKey, categoryObject); - - int desktopKey = category.behaviour.desktopKey; - if (desktopKey != -1) { - JSONObject behaviorObject = new JSONObject(); - behaviorObject.put("name", categoryKey); - behaviorObject.put("option", desktopKey); - categorySelectionsArray.put(behaviorObject); - } + ReVancedUtils.verifyOnMainThread(); + final SegmentCategory[] values = SegmentCategory.valuesWithoutUnsubmitted(); + CharSequence[] titles = new CharSequence[values.length]; + for (int i = 0; i < values.length; i++) { + titles[i] = values[i].getTitleWithColorDot(); } - json.put("userID", SettingsEnum.SB_UUID.getString()); - json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean()); - json.put("serverAddress", SettingsEnum.SB_API_URL.getString()); - json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean()); - json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()); - json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat()); - json.put("trackViewCount", SettingsEnum.SB_COUNT_SKIPS.getBoolean()); - json.put("categorySelections", categorySelectionsArray); - json.put("barTypes", barTypesObject); - return json.toString(); + new AlertDialog.Builder(context) + .setTitle(str("sb_new_segment_choose_category")) + .setItems(titles, (dialog, which) -> SBRequester.voteToChangeCategoryOnBackgroundThread(segment, values[which])) + .show(); } catch (Exception ex) { - LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast - Toast.makeText(context, str("settings_export_failed"), Toast.LENGTH_SHORT).show(); - return ""; + LogHelper.printException(() -> "onNewCategorySelect failure", ex); } } - public static boolean isSBButtonEnabled(Context context, String key) { - return SettingsEnum.SB_ENABLED.getBoolean() && SharedPrefHelper.getBoolean(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, key, false); + public static void onPreviewClicked() { + try { + ReVancedUtils.verifyOnMainThread(); + if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { + VideoInformation.seekTo(newSponsorSegmentStartMillis - 2500); + final SponsorSegment[] original = SegmentPlaybackController.getSegmentsOfCurrentVideo(); + final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1); + + segments[segments.length - 1] = new SponsorSegment(SegmentCategory.UNSUBMITTED, null, + newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, false); + + SegmentPlaybackController.setSegmentsOfCurrentVideo(segments); + } else { + ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first")); + } + } catch (Exception ex) { + LogHelper.printException(() -> "onPreviewClicked failure", ex); + } } - public enum VoteOption { - UPVOTE(str("vote_upvote"), false), - DOWNVOTE(str("vote_downvote"), true), - CATEGORY_CHANGE(str("vote_category"), true); - public final String title; - public final boolean shouldHighlight; - - - VoteOption(String title, boolean shouldHighlight) { - this.title = title; - this.shouldHighlight = shouldHighlight; + static void sendViewRequestAsync(@NonNull SponsorSegment segment) { + if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) { + return; } + segment.recordedAsSkipped = true; + final long totalTimeSkipped = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() + segment.length(); + SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(totalTimeSkipped); + SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt() + 1); + + if (SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()) { + ReVancedUtils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segment)); + } + } + + public static void onEditByHandClicked() { + try { + ReVancedUtils.verifyOnMainThread(); + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setTitle(str("sb_new_segment_edit_by_hand_title")) + .setMessage(str("sb_new_segment_edit_by_hand_content")) + .setNeutralButton(android.R.string.cancel, null) + .setNegativeButton(str("sb_new_segment_mark_start"), editByHandDialogListener) + .setPositiveButton(str("sb_new_segment_mark_end"), editByHandDialogListener) + .show(); + } catch (Exception ex) { + LogHelper.printException(() -> "onEditByHandClicked failure", ex); + } + } + + public static String getTimeSavedString(long totalSecondsSaved) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + Duration duration = Duration.ofSeconds(totalSecondsSaved); + final long hoursSaved = duration.toHours(); + final long minutesSaved = duration.toMinutes() % 60; + if (hoursSaved > 0) { + return str("sb_stats_saved_hour_format", hoursSaved, minutesSaved); + } + final long secondsSaved = duration.getSeconds() % 60; + if (minutesSaved > 0) { + return str("sb_stats_saved_minute_format", minutesSaved, secondsSaved); + } + return str("sb_stats_saved_second_format", secondsSaved); + } + return "error"; // will never be reached. YouTube requires Android O or greater } private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener { - public boolean settingStart; - public WeakReference editText; + boolean settingStart; + WeakReference editText; - @SuppressLint("DefaultLocale") @Override public void onClick(DialogInterface dialog, int which) { - final EditText editText = this.editText.get(); - if (editText == null) return; - Context context = ((AlertDialog) dialog).getContext(); - try { + final EditText editText = this.editText.get(); + if (editText == null) return; + long time = (which == DialogInterface.BUTTON_NEUTRAL) ? - getLastKnownVideoTime() : - (Objects.requireNonNull(dateFormatter.parse(editText.getText().toString())).getTime()); + VideoInformation.getVideoTime() : + (Objects.requireNonNull(manualEditTimeFormatter.parse(editText.getText().toString())).getTime()); if (settingStart) newSponsorSegmentStartMillis = Math.max(time, 0); @@ -646,10 +440,10 @@ public abstract class SponsorBlockUtils { editByHandDialogListener.onClick(dialog, settingStart ? DialogInterface.BUTTON_NEGATIVE : DialogInterface.BUTTON_POSITIVE); - else - Toast.makeText(context.getApplicationContext(), str("new_segment_edit_by_hand_saved"), Toast.LENGTH_SHORT).show(); } catch (ParseException e) { - Toast.makeText(context.getApplicationContext(), str("new_segment_edit_by_hand_parse_error"), Toast.LENGTH_LONG).show(); + ReVancedUtils.showToastLong(str("sb_new_segment_edit_by_hand_parse_error")); + } catch (Exception ex) { + LogHelper.printException(() -> "EditByHandSaveDialogListener failure", ex); } } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SwipeHelper.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SwipeHelper.java deleted file mode 100644 index aa3da868..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SwipeHelper.java +++ /dev/null @@ -1,90 +0,0 @@ -package app.revanced.integrations.sponsorblock; - -import android.content.Context; -import android.content.res.Resources; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import app.revanced.integrations.sponsorblock.player.PlayerType; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; - -public class SwipeHelper { - static FrameLayout _frameLayout; - public static boolean isTabletMode; - public static ViewGroup nextGenWatchLayout; - - public static void SetFrameLayout(Object obj) { - try { - _frameLayout = (FrameLayout) obj; - Context appContext = ReVancedUtils.getContext(); - if (ReVancedUtils.isTablet(appContext) || SharedPrefHelper.getBoolean(SharedPrefHelper.SharedPrefNames.YOUTUBE, "pref_swipe_tablet", false)) { - isTabletMode = true; - } - } catch (Exception e) { - LogHelper.printException(() -> "Unable to set FrameLayout", e); - } - } - - public static void setNextGenWatchLayout(Object obj) { - try { - nextGenWatchLayout = (ViewGroup) obj; - } catch (Exception e) { - LogHelper.printException(() -> "Unable to set _nextGenWatchLayout", e); - } - } - - public static boolean IsControlsShown() { - FrameLayout frameLayout; - if (isTabletMode || (frameLayout = _frameLayout) == null || frameLayout.getVisibility() != View.VISIBLE) { - return false; - } - try { - } catch (Exception e) { - LogHelper.printException(() -> "Unable to get related_endscreen_results visibility", e); - } - if (_frameLayout.getChildCount() > 0) { - return _frameLayout.getChildAt(0).getVisibility() == View.VISIBLE; - } - refreshLayout(); - return false; - } - - private static void refreshLayout() { - View findViewById; - try { - if (isWatchWhileFullScreen() && (findViewById = nextGenWatchLayout.findViewById(getIdentifier())) != null) { - _frameLayout = (FrameLayout) findViewById.getParent(); - LogHelper.printDebug(() -> "related_endscreen_results refreshed"); - } - } catch (Exception e) { - LogHelper.printException(() -> "Unable to refresh related_endscreen_results layout", e); - } - } - - - private static boolean isWatchWhileFullScreen() { - if (ReVancedUtils.getPlayerType() == null) { - return false; - } - return ReVancedUtils.getPlayerType() == PlayerType.WATCH_WHILE_FULLSCREEN; - } - - private static String getViewMessage(View view) { - try { - String resourceName = view.getResources() != null ? view.getId() != 0 ? view.getResources().getResourceName(view.getId()) : "no_id" : "no_resources"; - return "[" + view.getClass().getSimpleName() + "] " + resourceName + "\n"; - } catch (Resources.NotFoundException unused) { - return "[" + view.getClass().getSimpleName() + "] name_not_found\n"; - } - } - - private static int getIdentifier() { - Context appContext = ReVancedUtils.getContext(); - assert appContext != null; - return appContext.getResources().getIdentifier("related_endscreen_results", "id", appContext.getPackageName()); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/VotingButton.java b/app/src/main/java/app/revanced/integrations/sponsorblock/VotingButton.java deleted file mode 100644 index 99031c03..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/VotingButton.java +++ /dev/null @@ -1,118 +0,0 @@ -package app.revanced.integrations.sponsorblock; - -import android.content.Context; - -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ImageView; -import android.widget.RelativeLayout; - -import java.lang.ref.WeakReference; - -import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength; -import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -public class VotingButton { - static RelativeLayout _youtubeControlsLayout; - static WeakReference _votingButton = new WeakReference<>(null); - static int fadeDurationFast; - static int fadeDurationScheduled; - static Animation fadeIn; - static Animation fadeOut; - static boolean isShowing; - - public static void initialize(Object viewStub) { - try { - LogHelper.printDebug(() -> "initializing voting button"); - _youtubeControlsLayout = (RelativeLayout) viewStub; - - ImageView imageView = (ImageView) _youtubeControlsLayout - .findViewById(getIdentifier("voting_button", "id")); - - if (imageView == null) { - LogHelper.printDebug(() -> "Couldn't find imageView with \"voting_button\""); - } - if (imageView == null) return; - imageView.setOnClickListener(SponsorBlockUtils.voteButtonListener); - _votingButton = new WeakReference<>(imageView); - - // Animations - fadeDurationFast = getInteger("fade_duration_fast"); - fadeDurationScheduled = getInteger("fade_duration_scheduled"); - fadeIn = getAnimation("fade_in"); - fadeIn.setDuration(fadeDurationFast); - fadeOut = getAnimation("fade_out"); - fadeOut.setDuration(fadeDurationScheduled); - isShowing = true; - changeVisibilityImmediate(false); - } catch (Exception ex) { - LogHelper.printException(() -> "Unable to set RelativeLayout", ex); - } - } - - public static void changeVisibilityImmediate(boolean visible) { - changeVisibility(visible, true); - } - - public static void changeVisibilityNegatedImmediate(boolean visible) { - changeVisibility(!visible, true); - } - - public static void changeVisibility(boolean visible) { - changeVisibility(visible, false); - } - - public static void changeVisibility(boolean visible, boolean immediate) { - try { - if (isShowing == visible) return; - isShowing = visible; - - ImageView iView = _votingButton.get(); - if (_youtubeControlsLayout == null || iView == null) return; - - if (visible && shouldBeShown()) { - if (getLastKnownVideoTime() >= getCurrentVideoLength()) { - return; - } - LogHelper.printDebug(() -> "Fading in"); - iView.setVisibility(View.VISIBLE); - if (!immediate) - iView.startAnimation(fadeIn); - return; - } - - if (iView.getVisibility() == View.VISIBLE) { - LogHelper.printDebug(() -> "Fading out"); - if (!immediate) - iView.startAnimation(fadeOut); - iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE); - } - } catch (Exception ex) { - LogHelper.printException(() -> "changeVisibility failure", ex); - } - } - - static boolean shouldBeShown() { - return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean(); - } - - //region Helpers - private static int getIdentifier(String name, String defType) { - Context context = ReVancedUtils.getContext(); - return context.getResources().getIdentifier(name, defType, context.getPackageName()); - } - - private static int getInteger(String name) { - return ReVancedUtils.getContext().getResources().getInteger(getIdentifier(name, "integer")); - } - - private static Animation getAnimation(String name) { - return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), getIdentifier(name, "anim")); - } - //endregion -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java new file mode 100644 index 00000000..0e226662 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java @@ -0,0 +1,90 @@ +package app.revanced.integrations.sponsorblock.objects; + +import static app.revanced.integrations.utils.StringRef.sf; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.utils.StringRef; + +public enum CategoryBehaviour { + SKIP_AUTOMATICALLY("skip", 2, sf("sb_skip_automatically"), true), + // desktop does not have skip-once behavior. Key is unique to ReVanced + SKIP_AUTOMATICALLY_ONCE("skip-once", 4, sf("sb_skip_automatically_once"), true), + MANUAL_SKIP("manual-skip", 1, sf("sb_skip_showbutton"), false), + SHOW_IN_SEEKBAR("seekbar-only", 0, sf("sb_skip_seekbaronly"), false), + // Ignore is the default behavior if no desktop behavior key is present + IGNORE("ignore", 3, sf("sb_skip_ignore"), false); + + @NonNull + public final String key; + public final int desktopKey; + @NonNull + public final StringRef name; + /** + * If the segment should skip automatically + */ + public final boolean skip; + + CategoryBehaviour(String key, + int desktopKey, + StringRef name, + boolean skip) { + this.key = Objects.requireNonNull(key); + this.desktopKey = desktopKey; + this.name = Objects.requireNonNull(name); + this.skip = skip; + } + + @Nullable + public static CategoryBehaviour byStringKey(@NonNull String key) { + for (CategoryBehaviour behaviour : values()){ + if (behaviour.key.equals(key)) { + return behaviour; + } + } + return null; + } + + @Nullable + public static CategoryBehaviour byDesktopKey(int desktopKey) { + for (CategoryBehaviour behaviour : values()) { + if (behaviour.desktopKey == desktopKey) { + return behaviour; + } + } + return null; + } + + private static String[] behaviorKeys; + private static String[] behaviorNames; + + private static void createNameAndKeyArrays() { + ReVancedUtils.verifyOnMainThread(); + CategoryBehaviour[] behaviours = values(); + behaviorKeys = new String[behaviours.length]; + behaviorNames = new String[behaviours.length]; + for (int i = 0, length = behaviours.length; i < length; i++) { + CategoryBehaviour behaviour = behaviours[i]; + behaviorKeys[i] = behaviour.key; + behaviorNames[i] = behaviour.name.toString(); + } + } + + public static String[] getBehaviorNames() { + if (behaviorNames == null) { + createNameAndKeyArrays(); + } + return behaviorNames; + } + + public static String[] getBehaviorKeys() { + if (behaviorKeys == null) { + createNameAndKeyArrays(); + } + return behaviorKeys; + } +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/EditTextListPreference.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/EditTextListPreference.java deleted file mode 100644 index 5caacfdf..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/EditTextListPreference.java +++ /dev/null @@ -1,124 +0,0 @@ -package app.revanced.integrations.sponsorblock.objects; - -import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.formatColorString; -import static app.revanced.integrations.sponsorblock.StringRef.str; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.graphics.Color; -import android.preference.ListPreference; -import android.text.Editable; -import android.text.Html; -import android.text.InputType; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.widget.EditText; -import android.widget.Toast; - -import app.revanced.integrations.sponsorblock.SponsorBlockSettings; - -@SuppressWarnings("deprecation") -public class EditTextListPreference extends ListPreference { - - private EditText mEditText; - private int mClickedDialogEntryIndex; - - public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public EditTextListPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public EditTextListPreference(Context context) { - super(context); - } - - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - SponsorBlockSettings.SegmentInfo category = getCategoryBySelf(); - - mEditText = new EditText(builder.getContext()); - mEditText.setInputType(InputType.TYPE_CLASS_TEXT); - mEditText.setText(formatColorString(category.color)); - mEditText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - try { - Color.parseColor(s.toString()); // validation - getDialog().setTitle(Html.fromHtml(String.format(" %s", s, category.title))); - } catch (Exception ex) { - } - } - }); - builder.setView(mEditText); - builder.setTitle(category.getTitleWithDot()); - - builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { - EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); - }); - builder.setNeutralButton(str("reset"), (dialog, which) -> { - //EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_NEUTRAL); - int defaultColor = category.defaultColor; - category.setColor(defaultColor); - Toast.makeText(getContext().getApplicationContext(), str("color_reset"), Toast.LENGTH_SHORT).show(); - getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(defaultColor)).apply(); - reformatTitle(); - }); - builder.setNegativeButton(android.R.string.cancel, null); - - mClickedDialogEntryIndex = findIndexOfValue(getValue()); - builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which); - } - - @Override - protected void onDialogClosed(boolean positiveResult) { - if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) { - String value = getEntryValues()[mClickedDialogEntryIndex].toString(); - if (callChangeListener(value)) { - setValue(value); - } - String colorString = mEditText.getText().toString(); - SponsorBlockSettings.SegmentInfo category = getCategoryBySelf(); - if (colorString.equals(formatColorString(category.color))) { - return; - } - Context applicationContext = getContext().getApplicationContext(); - try { - int color = Color.parseColor(colorString); - category.setColor(color); - Toast.makeText(applicationContext, str("color_changed"), Toast.LENGTH_SHORT).show(); - getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(color)).apply(); - reformatTitle(); - } catch (Exception ex) { - Toast.makeText(applicationContext, str("color_invalid"), Toast.LENGTH_SHORT).show(); - } - } - } - - private SponsorBlockSettings.SegmentInfo getCategoryBySelf() { - return SponsorBlockSettings.SegmentInfo.byCategoryKey(getKey()); - } - - private String getColorPreferenceKey() { - return getKey() + SponsorBlockSettings.CATEGORY_COLOR_SUFFIX; - } - - private void reformatTitle() { - this.setTitle(getCategoryBySelf().getTitleWithDot()); - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java new file mode 100644 index 00000000..f23160a0 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java @@ -0,0 +1,305 @@ +package app.revanced.integrations.sponsorblock.objects; + +import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.IGNORE; +import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP; +import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; +import static app.revanced.integrations.utils.StringRef.sf; + +import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.Paint; +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.SharedPrefHelper; +import app.revanced.integrations.utils.StringRef; + +public enum SegmentCategory { + SPONSOR("sponsor", sf("sb_segments_sponsor"), sf("sb_segments_sponsor_sum"), sf("sb_skip_button_sponsor"), sf("sb_skipped_sponsor"), + SKIP_AUTOMATICALLY, 0x00D400), + SELF_PROMO("selfpromo", sf("sb_segments_selfpromo"), sf("sb_segments_selfpromo_sum"), sf("sb_skip_button_selfpromo"), sf("sb_skipped_selfpromo"), + SKIP_AUTOMATICALLY, 0xFFFF00), + INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"), + SKIP_AUTOMATICALLY, 0xCC00FF), + INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"), + sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"), + sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"), + MANUAL_SKIP, 0x00FFFF), + OUTRO("outro", sf("sb_segments_outro"), sf("sb_segments_outro_sum"), sf("sb_skip_button_outro"), sf("sb_skipped_outro"), + MANUAL_SKIP, 0x0202ED), + PREVIEW("preview", sf("sb_segments_preview"), sf("sb_segments_preview_sum"), + sf("sb_skip_button_preview_beginning"), sf("sb_skip_button_preview_middle"), sf("sb_skip_button_preview_end"), + sf("sb_skipped_preview_beginning"), sf("sb_skipped_preview_middle"), sf("sb_skipped_preview_end"), + IGNORE, 0x008FD6), + FILLER("filler", sf("sb_segments_filler"), sf("sb_segments_filler_sum"), sf("sb_skip_button_filler"), sf("sb_skipped_filler"), + IGNORE, 0x7300FF), + MUSIC_OFFTOPIC("music_offtopic", sf("sb_segments_nomusic"), sf("sb_segments_nomusic_sum"), sf("sb_skip_button_nomusic"), sf("sb_skipped_nomusic"), + MANUAL_SKIP, 0xFF9900), + UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("sb_skip_button_unsubmitted"), sf("sb_skipped_unsubmitted"), + SKIP_AUTOMATICALLY, 0xFFFFFF); + + private static final SegmentCategory[] mValuesWithoutUnsubmitted = new SegmentCategory[]{ + SPONSOR, + SELF_PROMO, + INTERACTION, + INTRO, + OUTRO, + PREVIEW, + FILLER, + MUSIC_OFFTOPIC, + }; + private static final Map mValuesMap = new HashMap<>(2 * mValuesWithoutUnsubmitted.length); + + private static final String COLOR_PREFERENCE_KEY_SUFFIX = "_color"; + + /** + * Categories currently enabled, formatted for an API call + */ + public static String sponsorBlockAPIFetchCategories = "[]"; + + static { + for (SegmentCategory value : mValuesWithoutUnsubmitted) + mValuesMap.put(value.key, value); + } + + @NonNull + public static SegmentCategory[] valuesWithoutUnsubmitted() { + return mValuesWithoutUnsubmitted; + } + + @Nullable + public static SegmentCategory byCategoryKey(@NonNull String key) { + return mValuesMap.get(key); + } + + public static void loadFromPreferences() { + SharedPreferences preferences = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK); + LogHelper.printDebug(() -> "loadFromPreferences"); + for (SegmentCategory category : valuesWithoutUnsubmitted()) { + category.load(preferences); + } + updateEnabledCategories(); + } + + /** + * Must be called if behavior of any category is changed + */ + public static void updateEnabledCategories() { + SegmentCategory[] categories = valuesWithoutUnsubmitted(); + List enabledCategories = new ArrayList<>(categories.length); + for (SegmentCategory category : categories) { + if (category.behaviour != CategoryBehaviour.IGNORE) { + enabledCategories.add(category.key); + } + } + + //"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]"; + if (enabledCategories.isEmpty()) + sponsorBlockAPIFetchCategories = "[]"; + else + sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]"; + } + + @NonNull + public final String key; + @NonNull + public final StringRef title; + @NonNull + public final StringRef description; + + /** + * Skip button text, if the skip occurs in the first quarter of the video + */ + @NonNull + public final StringRef skipButtonTextBeginning; + /** + * Skip button text, if the skip occurs in the middle half of the video + */ + @NonNull + public final StringRef skipButtonTextMiddle; + /** + * Skip button text, if the skip occurs in the last quarter of the video + */ + @NonNull + public final StringRef skipButtonTextEnd; + /** + * Skipped segment toast, if the skip occurred in the first quarter of the video + */ + @NonNull + public final StringRef skippedToastBeginning; + /** + * Skipped segment toast, if the skip occurred in the middle half of the video + */ + @NonNull + public final StringRef skippedToastMiddle; + /** + * Skipped segment toast, if the skip occurred in the last quarter of the video + */ + @NonNull + public final StringRef skippedToastEnd; + + @NonNull + public final Paint paint; + public final int defaultColor; + /** + * If value is changed, then also call {@link #save(SharedPreferences.Editor)} + */ + public int color; + /** + * If value is changed, then also call {@link #updateEnabledCategories()} + */ + @NonNull + public CategoryBehaviour behaviour; + + SegmentCategory(String key, StringRef title, StringRef description, + StringRef skipButtonText, + StringRef skippedToastText, + CategoryBehaviour defaultBehavior, int defaultColor) { + this(key, title, description, + skipButtonText, skipButtonText, skipButtonText, + skippedToastText, skippedToastText, skippedToastText, + defaultBehavior, defaultColor); + } + + SegmentCategory(String key, StringRef title, StringRef description, + StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd, + StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd, + CategoryBehaviour defaultBehavior, int defaultColor) { + this.key = Objects.requireNonNull(key); + this.title = Objects.requireNonNull(title); + this.description = Objects.requireNonNull(description); + this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning); + this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle); + this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd); + this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning); + this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle); + this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd); + this.behaviour = Objects.requireNonNull(defaultBehavior); + this.color = this.defaultColor = defaultColor; + this.paint = new Paint(); + setColor(defaultColor); + } + + /** + * Caller must also call {@link #updateEnabledCategories()} + */ + private void load(SharedPreferences preferences) { + String categoryColor = preferences.getString(key + COLOR_PREFERENCE_KEY_SUFFIX, null); + if (categoryColor == null) { + setColor(defaultColor); + } else { + setColor(categoryColor); + } + + String behaviorString = preferences.getString(key, null); + if (behaviorString != null) { + CategoryBehaviour preferenceBehavior = CategoryBehaviour.byStringKey(behaviorString); + if (preferenceBehavior == null) { + LogHelper.printException(() -> "Unknown behavior: " + behaviorString); // should never happen + } else { + behaviour = preferenceBehavior; + } + } + } + + /** + * Saves the current color and behavior. + * Calling code is responsible for calling {@link SharedPreferences.Editor#apply()} + */ + public void save(SharedPreferences.Editor editor) { + String colorString = (color == defaultColor) + ? null // remove any saved preference, so default is used on the next load + : colorString(); + editor.putString(key + COLOR_PREFERENCE_KEY_SUFFIX, colorString); + editor.putString(key, behaviour.key); + } + + /** + * @return HTML color format string + */ + @NonNull + public String colorString() { + return String.format("#%06X", color); + } + + public void setColor(@NonNull String colorString) throws IllegalArgumentException { + setColor(Color.parseColor(colorString)); + } + + public void setColor(int color) { + color &= 0xFFFFFF; + this.color = color; + paint.setColor(color); + paint.setAlpha(255); + } + + @NonNull + private static String getCategoryColorDotHTML(int color) { + color &= 0xFFFFFF; + return String.format("", color); + } + + @NonNull + public static Spanned getCategoryColorDot(int color) { + return Html.fromHtml(getCategoryColorDotHTML(color)); + } + + @NonNull + public Spanned getCategoryColorDot() { + return getCategoryColorDot(color); + } + + @NonNull + public Spanned getTitleWithColorDot() { + return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title); + } + + /** + * @param segmentStartTime video time the segment category started + * @param videoLength length of the video + * @return the skip button text + */ + @NonNull + public String getSkipButtonText(long segmentStartTime, long videoLength) { + if (videoLength == 0) { + return skipButtonTextBeginning.toString(); // video is still loading. Assume it's the beginning + } + final float position = segmentStartTime / (float) videoLength; + if (position < 0.25f) { + return skipButtonTextBeginning.toString(); + } else if (position < 0.75f) { + return skipButtonTextMiddle.toString(); + } + return skipButtonTextEnd.toString(); + } + + /** + * @param segmentStartTime video time the segment category started + * @param videoLength length of the video + * @return 'skipped segment' toast message + */ + @NonNull + public String getSkippedToastText(long segmentStartTime, long videoLength) { + if (videoLength == 0) { + return skippedToastBeginning.toString(); // video is still loading. Assume it's the beginning + } + final float position = segmentStartTime / (float) videoLength; + if (position < 0.25f) { + return skippedToastBeginning.toString(); + } else if (position < 0.75f) { + return skippedToastMiddle.toString(); + } + return skippedToastEnd.toString(); + } +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java new file mode 100644 index 00000000..829b2894 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java @@ -0,0 +1,155 @@ +package app.revanced.integrations.sponsorblock.objects; + +import static app.revanced.integrations.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.preference.ListPreference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import java.util.Objects; + +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public class SegmentCategoryListPreference extends ListPreference { + private final SegmentCategory category; + private EditText mEditText; + private int mClickedDialogEntryIndex; + + public SegmentCategoryListPreference(Context context, SegmentCategory category) { + super(context); + this.category = Objects.requireNonNull(category); + setKey(category.key); + setEntries(CategoryBehaviour.getBehaviorNames()); + setEntryValues(CategoryBehaviour.getBehaviorKeys()); + setSummary(category.description.toString()); + updateTitle(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + Context context = builder.getContext(); + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(70, 0, 150, 0); + + TableRow row = new TableRow(context); + + TextView colorTextLabel = new TextView(context); + colorTextLabel.setText(str("sb_color_dot_label")); + row.addView(colorTextLabel); + + TextView colorDotView = new TextView(context); + colorDotView.setText(category.getCategoryColorDot()); + colorDotView.setPadding(30, 0, 30, 0); + row.addView(colorDotView); + + mEditText = new EditText(context); + mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); + mEditText.setText(category.colorString()); + mEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + try { + String colorString = s.toString(); + if (!colorString.startsWith("#")) { + s.insert(0, "#"); // recursively calls back into this method + return; + } + if (colorString.length() > 7) { + s.delete(7, colorString.length()); + return; + } + final int color = Color.parseColor(colorString); + colorDotView.setText(SegmentCategory.getCategoryColorDot(color)); + } catch (IllegalArgumentException ex) { + // ignore + } + } + }); + mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + builder.setTitle(category.title.toString()); + + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + onClick(dialog, DialogInterface.BUTTON_POSITIVE); + }); + builder.setNeutralButton(str("sb_reset_color"), (dialog, which) -> { + try { + SharedPreferences.Editor editor = getSharedPreferences().edit(); + category.setColor(category.defaultColor); + category.save(editor); + editor.apply(); + updateTitle(); + ReVancedUtils.showToastShort(str("sb_color_reset")); + } catch (Exception ex) { + LogHelper.printException(() -> "setNeutralButton failure", ex); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + mClickedDialogEntryIndex = findIndexOfValue(getValue()); + builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which); + } catch (Exception ex) { + LogHelper.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + try { + if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) { + String value = getEntryValues()[mClickedDialogEntryIndex].toString(); + if (callChangeListener(value)) { + setValue(value); + category.behaviour = Objects.requireNonNull(CategoryBehaviour.byStringKey(value)); + SegmentCategory.updateEnabledCategories(); + } + String colorString = mEditText.getText().toString(); + try { + final int color = Color.parseColor(colorString) & 0xFFFFFF; + if (color != category.color) { + category.setColor(color); + ReVancedUtils.showToastShort(str("sb_color_changed")); + } + } catch (IllegalArgumentException ex) { + ReVancedUtils.showToastShort(str("sb_color_invalid")); + } + // behavior is already saved, but color needs to be saved + SharedPreferences.Editor editor = getSharedPreferences().edit(); + category.save(editor); + editor.apply(); + updateTitle(); + } + } catch (Exception ex) { + LogHelper.printException(() -> "onDialogClosed failure", ex); + } + } + + private void updateTitle() { + setTitle(category.getTitleWithColorDot()); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java index 585cd7ae..98156969 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java @@ -1,35 +1,130 @@ package app.revanced.integrations.sponsorblock.objects; +import static app.revanced.integrations.utils.StringRef.sf; + import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import java.text.MessageFormat; - -import app.revanced.integrations.sponsorblock.SponsorBlockSettings; +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.utils.StringRef; public class SponsorSegment implements Comparable { - public final long start; - public final long end; - public final SponsorBlockSettings.SegmentInfo category; - public final String UUID; - public final boolean isLocked; - public boolean didAutoSkipped = false; + public enum SegmentVote { + UPVOTE(sf("sb_vote_upvote"), 1,false), + DOWNVOTE(sf("sb_vote_downvote"), 0, true), + CATEGORY_CHANGE(sf("sb_vote_category"), -1, true); // apiVoteType is not used for category change - public SponsorSegment(long start, long end, SponsorBlockSettings.SegmentInfo category, String UUID, boolean isLocked) { - this.start = start; - this.end = end; - this.category = category; - this.UUID = UUID; - this.isLocked = isLocked; + @NonNull + public final StringRef title; + public final int apiVoteType; + public final boolean shouldHighlight; + + SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) { + this.title = title; + this.apiVoteType = apiVoteType; + this.shouldHighlight = shouldHighlight; + } } @NonNull - @Override - public String toString() { - return MessageFormat.format("SegmentInfo'{'start={0}, end={1}, category=''{2}'', locked={3}'}'", start, end, category, isLocked); + public final SegmentCategory category; + /** + * NULL if segment is unsubmitted + */ + @Nullable + public final String UUID; + public final long start; + public final long end; + public final boolean isLocked; + public boolean didAutoSkipped = false; + /** + * If this segment has been counted as 'skipped' + */ + public boolean recordedAsSkipped = false; + + public SponsorSegment(@NonNull SegmentCategory category, @Nullable String UUID, long start, long end, boolean isLocked) { + this.category = category; + this.UUID = UUID; + this.start = start; + this.end = end; + this.isLocked = isLocked; + } + + public boolean shouldAutoSkip() { + return category.behaviour.skip && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE); + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + */ + public boolean timeIsNearStart(long videoTime, long nearThreshold) { + return Math.abs(start - videoTime) <= nearThreshold; + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + */ + public boolean timeIsNearEnd(long videoTime, long nearThreshold) { + return Math.abs(end - videoTime) <= nearThreshold; + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment + * @return if the time parameter is within or close to this segment + */ + public boolean timeIsInsideOrNear(long videoTime, long nearThreshold) { + return (start - nearThreshold) <= videoTime && videoTime < (end + nearThreshold); + } + + /** + * @return if the time parameter is outside this segment + */ + public boolean timeIsOutside(long videoTime) { + return start < videoTime || end <= videoTime; + } + + /** + * @return if the segment is completely contained inside this segment + */ + public boolean containsSegment(SponsorSegment other) { + return start <= other.start && other.end <= end; + } + + /** + * @return the length of this segment, in milliseconds. Always a positive number. + */ + public long length() { + return end - start; + } + + /** + * @return 'skip segment' UI overlay button text + */ + @NonNull + public String getSkipButtonText() { + return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength()); + } + + /** + * @return 'skipped segment' toast message + */ + @NonNull + public String getSkippedToastText() { + return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()); } @Override public int compareTo(SponsorSegment o) { return (int) (this.start - o.start); } + + @NonNull + @Override + public String toString() { + return "SponsorSegment{" + + "category=" + category + + ", start=" + start + + ", end=" + end + + '}'; + } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java index 7aa11411..663fc6ae 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/UserStats.java @@ -1,31 +1,45 @@ package app.revanced.integrations.sponsorblock.objects; +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * SponsorBlock user stats + */ public class UserStats { - private final String userName; - private final double minutesSaved; - private final int segmentCount; - private final int viewCount; + @NonNull + public final String publicUserId; + @NonNull + public final String userName; + /** + * "User reputation". Unclear how SB determines this value. + */ + public final float reputation; + public final int segmentCount; + public final int viewCount; + public final double minutesSaved; - public UserStats(String userName, double minutesSaved, int segmentCount, int viewCount) { - this.userName = userName; - this.minutesSaved = minutesSaved; - this.segmentCount = segmentCount; - this.viewCount = viewCount; + public UserStats(@NonNull JSONObject json) throws JSONException { + publicUserId = json.getString("userID"); + userName = json.getString("userName"); + reputation = (float)json.getDouble("reputation"); + segmentCount = json.getInt("segmentCount"); + viewCount = json.getInt("viewCount"); + minutesSaved = json.getDouble("minutesSaved"); } - public String getUserName() { - return userName; - } - - public double getMinutesSaved() { - return minutesSaved; - } - - public int getSegmentCount() { - return segmentCount; - } - - public int getViewCount() { - return viewCount; + @NonNull + @Override + public String toString() { + return "UserStats{" + + "publicUserId='" + publicUserId + '\'' + + ", userName='" + userName + '\'' + + ", reputation=" + reputation + + ", segmentCount=" + segmentCount + + ", viewCount=" + viewCount + + ", minutesSaved=" + minutesSaved + + '}'; } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ChannelModel.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ChannelModel.java deleted file mode 100644 index 6c6f16ca..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ChannelModel.java +++ /dev/null @@ -1,29 +0,0 @@ -package app.revanced.integrations.sponsorblock.player; - -import java.io.Serializable; - -public class ChannelModel implements Serializable { - private String author; - private String channelId; - - public ChannelModel(String author, String channelId) { - this.author = author; - this.channelId = channelId; - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - - public String getChannelId() { - return channelId; - } - - public void setChannelId(String channelId) { - this.channelId = channelId; - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/PlayerType.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/PlayerType.java deleted file mode 100644 index 2ff9055e..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/PlayerType.java +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.integrations.sponsorblock.player; - -public enum PlayerType { - - NONE, - HIDDEN, - WATCH_WHILE_MINIMIZED, - WATCH_WHILE_MAXIMIZED, - WATCH_WHILE_FULLSCREEN, - WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN, - WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED, - WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, - WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED, - INLINE_MINIMAL, - VIRTUAL_REALITY_FULLSCREEN, - WATCH_WHILE_PICTURE_IN_PICTURE; - -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/ButtonVisibility.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/ButtonVisibility.java deleted file mode 100644 index 798c53f9..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/ButtonVisibility.java +++ /dev/null @@ -1,40 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -import android.content.Context; - -import app.revanced.integrations.utils.SharedPrefHelper; - -public class ButtonVisibility { - public static Visibility getButtonVisibility(String key) { - return getButtonVisibility(key, SharedPrefHelper.SharedPrefNames.YOUTUBE); - } - - public static Visibility getButtonVisibility(String key, SharedPrefHelper.SharedPrefNames name) { - String value = SharedPrefHelper.getString(name, key, null); - - if (value == null || value.isEmpty()) return Visibility.NONE; - - switch (value.toUpperCase()) { - case "PLAYER": - return Visibility.PLAYER; - case "BUTTON_CONTAINER": - return Visibility.BUTTON_CONTAINER; - case "BOTH": - return Visibility.BOTH; - default: - return Visibility.NONE; - } - } - - public static boolean isVisibleInContainer(String key) { - return isVisibleInContainer(getButtonVisibility(key)); - } - - public static boolean isVisibleInContainer(String key, SharedPrefHelper.SharedPrefNames name) { - return isVisibleInContainer(getButtonVisibility(key, name)); - } - - public static boolean isVisibleInContainer(Visibility visibility) { - return visibility == Visibility.BOTH || visibility == Visibility.BUTTON_CONTAINER; - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/NewSegmentLayout.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/NewSegmentLayout.java deleted file mode 100644 index a429d6cd..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/NewSegmentLayout.java +++ /dev/null @@ -1,151 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.graphics.drawable.RippleDrawable; -import android.util.AttributeSet; - -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.sponsorblock.NewSegmentHelperLayout; -import app.revanced.integrations.sponsorblock.PlayerController; -import app.revanced.integrations.sponsorblock.SponsorBlockUtils; - -public class NewSegmentLayout extends FrameLayout { - - private LinearLayout newSegmentContainer; - public int defaultBottomMargin; - public int ctaBottomMargin; - public ImageButton rewindButton; - public ImageButton forwardButton; - public ImageButton adjustButton; - public ImageButton compareButton; - public ImageButton editButton; - public ImageButton publishButton; - private int rippleEffectId; - - public NewSegmentLayout(Context context) { - super(context); - this.initialize(context); - } - - public NewSegmentLayout(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - this.initialize(context); - } - - public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr) { - super(context, attributeSet, defStyleAttr); - this.initialize(context); - } - - public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { - super(context, attributeSet, defStyleAttr, defStyleRes); - this.initialize(context); - } - - private final void initialize(Context context) { - LayoutInflater.from(context).inflate(getIdentifier(context, "new_segment", "layout"), this, true); - Resources resources = context.getResources(); - - TypedValue rippleEffect = new TypedValue(); - getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true); - rippleEffectId = rippleEffect.resourceId; - - this.newSegmentContainer = (LinearLayout) this.findViewById(getIdentifier(context, "new_segment_container", "id")); - - this.rewindButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_rewind", "id")); - if (this.rewindButton != null) { - setClickEffect(this.rewindButton); - this.rewindButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Rewind button clicked"); - PlayerController.skipRelativeMilliseconds(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()); - } - }); - } - this.forwardButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_forward", "id")); - if (this.forwardButton != null) { - setClickEffect(this.forwardButton); - this.forwardButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Forward button clicked"); - PlayerController.skipRelativeMilliseconds(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()); - } - }); - } - this.adjustButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_adjust", "id")); - if (this.adjustButton != null) { - setClickEffect(this.adjustButton); - this.adjustButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Adjust button clicked"); - SponsorBlockUtils.onMarkLocationClicked(NewSegmentHelperLayout.context); - } - }); - } - this.compareButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_compare", "id")); - if (this.compareButton != null) { - setClickEffect(this.compareButton); - this.compareButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Compare button clicked"); - SponsorBlockUtils.onPreviewClicked(NewSegmentHelperLayout.context); - } - }); - } - this.editButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_edit", "id")); - if (this.editButton != null) { - setClickEffect(this.editButton); - this.editButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Edit button clicked"); - SponsorBlockUtils.onEditByHandClicked(NewSegmentHelperLayout.context); - } - }); - } - this.publishButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_publish", "id")); - if (this.publishButton != null) { - setClickEffect(this.publishButton); - this.publishButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Publish button clicked"); - SponsorBlockUtils.onPublishClicked(NewSegmentHelperLayout.context); - } - }); - } - - this.defaultBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "brand_interaction_default_bottom_margin", "dimen")); - this.ctaBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "brand_interaction_cta_bottom_margin", "dimen")); - } - - private void setClickEffect(ImageButton btn) { - btn.setBackgroundResource(rippleEffectId); - - RippleDrawable rippleDrawable = (RippleDrawable) btn.getBackground(); - - int[][] states = new int[][]{new int[]{android.R.attr.state_enabled}}; - int[] colors = new int[]{0x33ffffff}; // sets the ripple color to white - - ColorStateList colorStateList = new ColorStateList(states, colors); - rippleDrawable.setColor(colorStateList); - } - - private int getIdentifier(Context context, String name, String defType) { - return context.getResources().getIdentifier(name, defType, context.getPackageName()); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SkipSponsorButton.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SkipSponsorButton.java deleted file mode 100644 index 2136bb7a..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SkipSponsorButton.java +++ /dev/null @@ -1,124 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.ColorDrawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.sponsorblock.PlayerController; - -public class SkipSponsorButton extends FrameLayout { - public CharSequence skipSponsorTextViewText; - public CharSequence skipSponsorText; - public ImageView skipSponsorButtonIcon; - public TextView skipSponsorTextView; - public int currentTextColor; - public int invertedButtonForegroundColor; - public int backgroundColor; - public int invertedBackgroundColor; - public ColorDrawable backgroundColorDrawable; - public int defaultBottomMargin; - public int ctaBottomMargin; - private LinearLayout skipSponsorBtnContainer; - private final Paint background; - private final Paint border; - private boolean highContrast = true; - - public SkipSponsorButton(Context context) { - super(context); - this.background = new Paint(); - this.border = new Paint(); - this.initialize(context); - } - - public SkipSponsorButton(Context context, AttributeSet attributeSet) { - super(context, attributeSet); - this.background = new Paint(); - this.border = new Paint(); - this.initialize(context); - } - - public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) { - super(context, attributeSet, defStyleAttr); - this.background = new Paint(); - this.border = new Paint(); - this.initialize(context); - } - - public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { - super(context, attributeSet, defStyleAttr, defStyleRes); - this.background = new Paint(); - this.border = new Paint(); - this.initialize(context); - } - - private final void initialize(Context context) { - LayoutInflater.from(context).inflate(getIdentifier(context, "skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button - this.setMinimumHeight(this.getResources().getDimensionPixelSize(getIdentifier(context, "ad_skip_ad_button_min_height", "dimen"))); // dimen:ad_skip_ad_button_min_height - this.skipSponsorBtnContainer = (LinearLayout) this.findViewById(getIdentifier(context, "skip_sponsor_button_container", "id")); // id:skip_ad_button_container - this.skipSponsorButtonIcon = (ImageView) this.findViewById(getIdentifier(context, "skip_sponsor_button_icon", "id")); // id:skip_ad_button_icon - this.backgroundColor = getColor(context, getIdentifier(context, "skip_ad_button_background_color", "color")); // color:skip_ad_button_background_color - this.invertedBackgroundColor = getColor(context, getIdentifier(context, "skip_ad_button_inverted_background_color", "color")); // color:skip_ad_button_inverted_background_color - this.background.setColor(this.backgroundColor); - this.background.setStyle(Paint.Style.FILL); - int borderColor = getColor(context, getIdentifier(context, "skip_ad_button_border_color", "color")); // color:skip_ad_button_border_color - this.border.setColor(borderColor); - float borderWidth = this.getResources().getDimension(getIdentifier(context, "ad_skip_ad_button_border_width", "dimen")); // dimen:ad_skip_ad_button_border_width - this.border.setStrokeWidth(borderWidth); - this.border.setStyle(Paint.Style.STROKE); - TextView skipSponsorText = (TextView) this.findViewById(getIdentifier(context, "skip_sponsor_button_text", "id")); // id:skip_ad_button_text - this.skipSponsorTextView = skipSponsorText; - this.skipSponsorTextViewText = skipSponsorText.getText(); - this.currentTextColor = this.skipSponsorTextView.getCurrentTextColor(); - this.invertedButtonForegroundColor = getColor(context, getIdentifier(context, "skip_ad_button_inverted_foreground_color", "color")); // color:skip_ad_button_inverted_foreground_color - this.backgroundColorDrawable = new ColorDrawable(this.backgroundColor); - Resources resources = context.getResources(); - this.defaultBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "skip_button_default_bottom_margin", "dimen")); // dimen:skip_button_default_bottom_margin - this.ctaBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "skip_button_cta_bottom_margin", "dimen")); // dimen:skip_button_cta_bottom_margin - this.skipSponsorText = resources.getText(getIdentifier(context, "skip_sponsor", "string")); // string:skip_ads "Skip ads" - - this.skipSponsorBtnContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogHelper.printDebug(() -> "Skip button clicked"); - PlayerController.onSkipSponsorClicked(); - } - }); - } - - @Override // android.view.ViewGroup - protected final void dispatchDraw(Canvas canvas) { - int width = this.skipSponsorBtnContainer.getWidth(); - int height = this.skipSponsorBtnContainer.getHeight(); - int top = this.skipSponsorBtnContainer.getTop(); - int left = this.skipSponsorBtnContainer.getLeft(); - float floatLeft = (float) left; - float floatTop = (float) top; - float floatWidth = (float) (left + width); - float floatHeight = (float) (top + height); - canvas.drawRect(floatLeft, floatTop, floatWidth, floatHeight, this.background); - if (!this.highContrast) { - canvas.drawLines(new float[]{floatWidth, floatTop, floatLeft, floatTop, floatLeft, floatTop, floatLeft, floatHeight, floatLeft, floatHeight, floatWidth, floatHeight}, this.border); - } - - super.dispatchDraw(canvas); - } - - - public static int getColor(Context context, int arg3) { - return context.getColor(arg3); - } - - private int getIdentifier(Context context, String name, String defType) { - return context.getResources().getIdentifier(name, defType, context.getPackageName()); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SlimButton.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SlimButton.java deleted file mode 100644 index 2827f856..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SlimButton.java +++ /dev/null @@ -1,67 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -import android.content.Context; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -public abstract class SlimButton implements View.OnClickListener { - public static int SLIM_METADATA_BUTTON_ID; - public final View view; - public final Context context; - private final ViewGroup container; - protected final ImageView button_icon; - protected final TextView button_text; - private boolean viewAdded = false; - - static { - SLIM_METADATA_BUTTON_ID = ReVancedUtils.getIdentifier("slim_metadata_button", "layout"); - } - - public SlimButton(Context context, ViewGroup container, int id, boolean visible) { - LogHelper.printDebug(() -> "Adding button with id " + id + " and visibility of " + visible); - this.context = context; - this.container = container; - view = LayoutInflater.from(context).inflate(id, container, false); - button_icon = (ImageView) view.findViewById(ReVancedUtils.getIdentifier("button_icon", "id")); - button_text = (TextView) view.findViewById(ReVancedUtils.getIdentifier("button_text", "id")); - - view.setOnClickListener(this); - - setVisible(visible); - } - - public void setVisible(boolean visible) { - try { - if (!viewAdded && visible) { - container.addView(view); - viewAdded = true; - } else if (viewAdded && !visible) { - container.removeView(view); - viewAdded = false; - } - setContainerVisibility(); - } catch (Exception ex) { - LogHelper.printException(() -> "Error while changing button visibility", ex); - } - } - - private void setContainerVisibility() { - if (container == null) return; - - for (int i = 0; i < container.getChildCount(); i++) { - if (container.getChildAt(i).getVisibility() == View.VISIBLE) { - container.setVisibility(View.VISIBLE); - return; - } - } - - container.setVisibility(View.GONE); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockView.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockView.java deleted file mode 100644 index f7bfef5a..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockView.java +++ /dev/null @@ -1,175 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.RelativeLayout; - -import java.lang.ref.WeakReference; - -import app.revanced.integrations.sponsorblock.player.PlayerType; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.sponsorblock.SwipeHelper; - -public class SponsorBlockView { - - static RelativeLayout inlineSponsorOverlay; - static ViewGroup _youtubeOverlaysLayout; - static WeakReference _skipSponsorButton = new WeakReference<>(null); - static WeakReference _newSegmentLayout = new WeakReference<>(null); - static boolean shouldShowOnPlayerType = true; - - public static void initialize(Object viewGroup) { - try { - LogHelper.printDebug(() -> "initializing"); - - _youtubeOverlaysLayout = (ViewGroup) viewGroup; - - addView(); - } catch (Exception ex) { - LogHelper.printException(() -> "Unable to set ViewGroup", ex); - } - } - - public static void showSkipButton() { - skipSponsorButtonVisibility(true); - } - - public static void hideSkipButton() { - skipSponsorButtonVisibility(false); - } - - public static void showNewSegmentLayout() { - newSegmentLayoutVisibility(true); - } - - public static void hideNewSegmentLayout() { - newSegmentLayoutVisibility(false); - } - - public static void playerTypeChanged(PlayerType playerType) { - try { - shouldShowOnPlayerType = (playerType == PlayerType.WATCH_WHILE_FULLSCREEN || playerType == PlayerType.WATCH_WHILE_MAXIMIZED); - - if (playerType == PlayerType.WATCH_WHILE_FULLSCREEN) { - setSkipBtnMargins(true); - setNewSegmentLayoutMargins(true); - return; - } - - setSkipBtnMargins(false); - setNewSegmentLayoutMargins(false); - } catch (Exception ex) { - LogHelper.printException(() -> "Player type changed caused a crash.", ex); - } - } - - private static void addView() { - inlineSponsorOverlay = new RelativeLayout(ReVancedUtils.getContext()); - setLayoutParams(inlineSponsorOverlay); - LayoutInflater.from(ReVancedUtils.getContext()).inflate(getIdentifier("inline_sponsor_overlay", "layout"), inlineSponsorOverlay); - - _youtubeOverlaysLayout.addView(inlineSponsorOverlay, _youtubeOverlaysLayout.getChildCount() - 2); - - SkipSponsorButton skipSponsorButton = (SkipSponsorButton) inlineSponsorOverlay.findViewById(getIdentifier("skip_sponsor_button", "id")); - _skipSponsorButton = new WeakReference<>(skipSponsorButton); - - NewSegmentLayout newSegmentView = (NewSegmentLayout) inlineSponsorOverlay.findViewById(getIdentifier("new_segment_view", "id")); - _newSegmentLayout = new WeakReference<>(newSegmentView); - } - - private static void setLayoutParams(RelativeLayout relativeLayout) { - relativeLayout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); - } - - private static void setSkipBtnMargins(boolean fullScreen) { - SkipSponsorButton skipSponsorButton = _skipSponsorButton.get(); - if (skipSponsorButton == null) { - LogHelper.printException(() -> "Unable to setSkipBtnMargins"); - return; - } - - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams(); - if (params == null) { - LogHelper.printException(() -> "Unable to setSkipBtnMargins"); - return; - } - params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin; - skipSponsorButton.setLayoutParams(params); - } - - private static void skipSponsorButtonVisibility(boolean visible) { - SkipSponsorButton skipSponsorButton = _skipSponsorButton.get(); - if (skipSponsorButton == null) { - LogHelper.printException(() -> "Unable to skipSponsorButtonVisibility"); - return; - } - - visible &= shouldShowOnPlayerType; - - skipSponsorButton.setVisibility(visible ? View.VISIBLE : View.GONE); - bringLayoutToFront(); - } - - private static void setNewSegmentLayoutMargins(boolean fullScreen) { - NewSegmentLayout newSegmentLayout = _newSegmentLayout.get(); - if (newSegmentLayout == null) { - LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins"); - return; - } - - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams(); - if (params == null) { - LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins"); - return; - } - params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin; - newSegmentLayout.setLayoutParams(params); - } - - private static void newSegmentLayoutVisibility(boolean visible) { - NewSegmentLayout newSegmentLayout = _newSegmentLayout.get(); - if (newSegmentLayout == null) { - LogHelper.printException(() -> "Unable to newSegmentLayoutVisibility"); - return; - } - - visible &= shouldShowOnPlayerType; - - newSegmentLayout.setVisibility(visible ? View.VISIBLE : View.GONE); - bringLayoutToFront(); - } - - private static void bringLayoutToFront() { - checkLayout(); - inlineSponsorOverlay.bringToFront(); - inlineSponsorOverlay.requestLayout(); - inlineSponsorOverlay.invalidate(); - } - - private static void checkLayout() { - if (inlineSponsorOverlay.getHeight() == 0) { - ViewGroup watchLayout = SwipeHelper.nextGenWatchLayout; - if (watchLayout == null) { - LogHelper.printDebug(() -> "nextGenWatchLayout is null!"); - return; - } - View layout = watchLayout.findViewById(getIdentifier("player_overlays", "id")); - - if (layout == null) { - LogHelper.printDebug(() -> "player_overlays was not found for SB"); - return; - } - - initialize(layout); - LogHelper.printDebug(() -> "player_overlays refreshed for SB"); - } - } - - private static int getIdentifier(String name, String defType) { - Context context = ReVancedUtils.getContext(); - return context.getResources().getIdentifier(name, defType, context.getPackageName()); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockVoting.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockVoting.java deleted file mode 100644 index 13c876c8..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/SponsorBlockVoting.java +++ /dev/null @@ -1,26 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import app.revanced.integrations.utils.ReVancedUtils; - -public class SponsorBlockVoting extends SlimButton { - public SponsorBlockVoting(Context context, ViewGroup container) { - super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false); - - initialize(); - } - - private void initialize() { - this.button_icon.setImageResource(ReVancedUtils.getIdentifier("revanced_sb_voting", "drawable")); - this.button_text.setText("SB Voting"); - } - - @Override - public void onClick(View view) { - Toast.makeText(ReVancedUtils.getContext(), "Nothing atm", Toast.LENGTH_SHORT).show(); - } -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/Visibility.java b/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/Visibility.java deleted file mode 100644 index fe22b085..00000000 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/player/ui/Visibility.java +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.integrations.sponsorblock.player.ui; - -public enum Visibility { - NONE, - PLAYER, - BUTTON_CONTAINER, - BOTH, -} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java index e53003c3..f235814a 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java @@ -1,22 +1,17 @@ package app.revanced.integrations.sponsorblock.requests; -import static android.text.Html.fromHtml; -import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.timeWithoutSegments; -import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.videoHasSegments; -import static app.revanced.integrations.sponsorblock.StringRef.str; -import static app.revanced.integrations.utils.ReVancedUtils.runOnMainThread; +import static app.revanced.integrations.utils.StringRef.str; -import android.content.Context; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -25,207 +20,253 @@ import java.util.concurrent.TimeUnit; import app.revanced.integrations.requests.Requester; import app.revanced.integrations.requests.Route; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.sponsorblock.PlayerController; -import app.revanced.integrations.sponsorblock.SponsorBlockSettings; -import app.revanced.integrations.sponsorblock.SponsorBlockUtils; -import app.revanced.integrations.sponsorblock.SponsorBlockUtils.VoteOption; +import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; +import app.revanced.integrations.sponsorblock.objects.SegmentCategory; import app.revanced.integrations.sponsorblock.objects.SponsorSegment; +import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote; import app.revanced.integrations.sponsorblock.objects.UserStats; -import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; public class SBRequester { private static final String TIME_TEMPLATE = "%.3f"; + /** + * TCP timeout + */ + private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 7000; + + /** + * HTTP response timeout + */ + private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 10000; + + /** + * Response code of a successful API call + */ + private static final int HTTP_STATUS_CODE_SUCCESS = 200; + private SBRequester() { } - public static synchronized SponsorSegment[] getSegments(String videoId) { + @NonNull + public static SponsorSegment[] getSegments(@NonNull String videoId) { + ReVancedUtils.verifyOffMainThread(); List segments = new ArrayList<>(); try { - HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories); - int responseCode = connection.getResponseCode(); - runVipCheck(); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SegmentCategory.sponsorBlockAPIFetchCategories); + final int responseCode = connection.getResponseCode(); - if (responseCode == 200) { - // FIXME? should this use Requester#getJSONArray and not disconnect? - // HTTPURLConnection#disconnect says: - // disconnect if other requests to the server - // are unlikely in the near future. - JSONArray responseArray = Requester.parseJSONArrayAndDisconnect(connection); - int length = responseArray.length(); - for (int i = 0; i < length; i++) { + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + JSONArray responseArray = Requester.parseJSONArray(connection); + final long minSegmentDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000); + for (int i = 0, length = responseArray.length(); i < length; i++) { JSONObject obj = (JSONObject) responseArray.get(i); JSONArray segment = obj.getJSONArray("segment"); - long start = (long) (segment.getDouble(0) * 1000); - long end = (long) (segment.getDouble(1) * 1000); - - long minDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000); - if ((end - start) < minDuration) + final long start = (long) (segment.getDouble(0) * 1000); + final long end = (long) (segment.getDouble(1) * 1000); + if ((end - start) < minSegmentDuration) continue; - String category = obj.getString("category"); + String categoryKey = obj.getString("category"); String uuid = obj.getString("UUID"); boolean locked = obj.getInt("locked") == 1; - SponsorBlockSettings.SegmentInfo segmentCategory = SponsorBlockSettings.SegmentInfo.byCategoryKey(category); - if (segmentCategory != null && segmentCategory.behaviour.showOnTimeBar) { - SponsorSegment sponsorSegment = new SponsorSegment(start, end, segmentCategory, uuid, locked); + SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(categoryKey); + if (segmentCategory == null) { + LogHelper.printException(() -> "Received unknown category: " + categoryKey); // should never happen + } else if (segmentCategory.behaviour != CategoryBehaviour.IGNORE) { + SponsorSegment sponsorSegment = new SponsorSegment(segmentCategory, uuid, start, end, locked); segments.add(sponsorSegment); } } - if (!segments.isEmpty()) { - videoHasSegments = true; - timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0])); - } + LogHelper.printDebug(() -> { + StringBuilder builder = new StringBuilder("Downloaded segments:"); + for (SponsorSegment segment : segments) { + builder.append('\n').append(segment); + } + return builder.toString(); + }); + runVipCheckInBackgroundIfNeeded(); + } else if (responseCode == 404) { + // no segments are found. a normal response + LogHelper.printDebug(() -> "No segments found for video: " + videoId); + } else { + LogHelper.printException(() -> "getSegments failed with response code: " + responseCode, + null, str("sb_sponsorblock_connection_failure_status", responseCode)); + connection.disconnect(); // something went wrong, might as well disconnect } - connection.disconnect(); + } catch (SocketTimeoutException ex) { + LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_timeout")); } catch (Exception ex) { - LogHelper.printException(() -> "failed to get segments", ex); + LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_generic")); } return segments.toArray(new SponsorSegment[0]); } - public static void submitSegments(String videoId, String uuid, float startTime, float endTime, String category, Runnable toastRunnable) { + public static void submitSegments(@NonNull String userPrivateId, @NonNull String videoId, @NonNull String category, + long startTime, long endTime, long videoLength) { + ReVancedUtils.verifyOffMainThread(); try { - String start = String.format(Locale.US, TIME_TEMPLATE, startTime); - String end = String.format(Locale.US, TIME_TEMPLATE, endTime); - String duration = String.valueOf(PlayerController.getCurrentVideoLength() / 1000); - HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, videoId, uuid, start, end, category, duration); - int responseCode = connection.getResponseCode(); + String start = String.format(Locale.US, TIME_TEMPLATE, startTime / 1000f); + String end = String.format(Locale.US, TIME_TEMPLATE, endTime / 1000f); + String duration = String.format(Locale.US, TIME_TEMPLATE, videoLength / 1000f); + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, userPrivateId, videoId, category, start, end, duration); + final int responseCode = connection.getResponseCode(); + + final String messageToToast; switch (responseCode) { - case 200: - SponsorBlockUtils.messageToToast = str("submit_succeeded"); + case HTTP_STATUS_CODE_SUCCESS: + messageToToast = str("sb_submit_succeeded"); break; case 409: - SponsorBlockUtils.messageToToast = str("submit_failed_duplicate"); + messageToToast = str("sb_submit_failed_duplicate"); break; case 403: - SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); + messageToToast = str("sb_submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); break; case 429: - SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit"); + messageToToast = str("sb_submit_failed_rate_limit"); break; case 400: - SponsorBlockUtils.messageToToast = str("submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection)); + messageToToast = str("sb_submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection)); break; default: - SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage()); + messageToToast = str("sb_submit_failed_unknown_error", responseCode, connection.getResponseMessage()); break; } - runOnMainThread(toastRunnable); - connection.disconnect(); + ReVancedUtils.showToastLong(messageToToast); + } catch (SocketTimeoutException ex) { + ReVancedUtils.showToastLong(str("sb_submit_failed_timeout")); } catch (Exception ex) { LogHelper.printException(() -> "failed to submit segments", ex); } } - public static void sendViewCountRequest(SponsorSegment segment) { + public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) { + ReVancedUtils.verifyOffMainThread(); try { HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID); - connection.disconnect(); + final int responseCode = connection.getResponseCode(); + + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + LogHelper.printDebug(() -> "Successfully sent view count for segment: " + segment); + } else { + LogHelper.printDebug(() -> "Failed to sent view count for segment: " + segment.UUID + + " responseCode: " + responseCode); // debug level, no toast is shown + } + } catch (IOException ex) { + LogHelper.printInfo(() -> "Failed to send view count", ex); // do not show a toast } catch (Exception ex) { - LogHelper.printException(() -> "failed to send view count request", ex); + LogHelper.printException(() -> "Failed to send view count request", ex); // should never happen } } - public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, String... args) { + public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) { + voteOrRequestCategoryChange(segment, voteOption, null); + } + public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) { + voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor); + } + private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) { ReVancedUtils.runOnBackgroundThread(() -> { try { String segmentUuid = segment.UUID; String uuid = SettingsEnum.SB_UUID.getString(); - String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0); - - HttpURLConnection connection = voteOption == VoteOption.CATEGORY_CHANGE - ? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, segmentUuid, uuid, args[0]) - : getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, segmentUuid, uuid, vote); - int responseCode = connection.getResponseCode(); + HttpURLConnection connection = (voteOption == SegmentVote.CATEGORY_CHANGE) + ? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, uuid, segmentUuid, categoryToVoteFor.key) + : getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, uuid, segmentUuid, String.valueOf(voteOption.apiVoteType)); + final int responseCode = connection.getResponseCode(); switch (responseCode) { - case 200: - SponsorBlockUtils.messageToToast = str("vote_succeeded"); + case HTTP_STATUS_CODE_SUCCESS: + LogHelper.printDebug(() -> "Vote success for segment: " + segment); break; case 403: - SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); + ReVancedUtils.showToastLong( + str("sb_vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection))); break; default: - SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage()); + ReVancedUtils.showToastLong( + str("sb_vote_failed_unknown_error", responseCode, connection.getResponseMessage())); break; } - runOnMainThread(() -> Toast.makeText(context, SponsorBlockUtils.messageToToast, Toast.LENGTH_LONG).show()); - connection.disconnect(); + } catch (SocketTimeoutException ex) { + LogHelper.printException(() -> "failed to vote for segment", ex, str("sb_vote_failed_timeout")); } catch (Exception ex) { - LogHelper.printException(() -> "failed to vote for segment", ex); + LogHelper.printException(() -> "failed to vote for segment", ex); // should never happen } }); } - public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) { - if (!SettingsEnum.SB_ENABLED.getBoolean()) { - loadingPreference.setTitle(str("stats_sb_disabled")); - return; + /** + * @return NULL, if stats fetch failed + */ + @Nullable + public static UserStats retrieveUserStats() { + ReVancedUtils.verifyOffMainThread(); + try { + UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString())); + LogHelper.printDebug(() -> "user stats: " + stats); + return stats; + } catch (IOException ex) { + LogHelper.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast + } catch (Exception ex) { + LogHelper.printException(() -> "failure retrieving user stats", ex); // should never happen } - - ReVancedUtils.runOnBackgroundThread(() -> { - try { - JSONObject json = getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString()); - UserStats stats = new UserStats(json.getString("userName"), json.getDouble("minutesSaved"), json.getInt("segmentCount"), - json.getInt("viewCount")); - runOnMainThread(() -> { // get back on main thread to modify UI elements - SponsorBlockUtils.addUserStats(category, loadingPreference, stats); - }); - } catch (Exception ex) { - LogHelper.printException(() -> "failed to retrieve user stats", ex); - } - }); + return null; } - public static void setUsername(String username, EditTextPreference preference, Runnable toastRunnable) { - ReVancedUtils.runOnBackgroundThread(() -> { - try { - HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username); - int responseCode = connection.getResponseCode(); - - if (responseCode == 200) { - SponsorBlockUtils.messageToToast = str("stats_username_changed"); - runOnMainThread(() -> { - preference.setTitle(fromHtml(str("stats_username", username))); - preference.setText(username); - }); - } else { - SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage()); - } - runOnMainThread(toastRunnable); - connection.disconnect(); - } catch (Exception ex) { - LogHelper.printException(() -> "failed to set username", ex); + /** + * @return NULL if the call was successful. If unsuccessful, an error message is returned. + */ + @Nullable + public static String setUsername(@NonNull String username) { + ReVancedUtils.verifyOffMainThread(); + try { + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username); + final int responseCode = connection.getResponseCode(); + String responseMessage = connection.getResponseMessage(); + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + return null; } - }); + return str("sb_stats_username_change_unknown_error", responseCode, responseMessage); + } catch (Exception ex) { // should never happen + LogHelper.printInfo(() -> "failed to set username", ex); // do not toast + return str("sb_stats_username_change_unknown_error", 0, ex.getMessage()); + } } - public static void runVipCheck() { + public static void runVipCheckInBackgroundIfNeeded() { long now = System.currentTimeMillis(); if (now < (SettingsEnum.SB_LAST_VIP_CHECK.getLong() + TimeUnit.DAYS.toMillis(3))) { return; } - try { - JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString()); - boolean vip = json.getBoolean("vip"); - SettingsEnum.SB_IS_VIP.saveValue(vip); - SettingsEnum.SB_LAST_VIP_CHECK.saveValue(now); - } catch (Exception ex) { - LogHelper.printException(() -> "failed to check VIP", ex); - } + ReVancedUtils.runOnBackgroundThread(() -> { + try { + JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString()); + boolean vip = json.getBoolean("vip"); + SettingsEnum.SB_IS_VIP.saveValue(vip); + SettingsEnum.SB_LAST_VIP_CHECK.saveValue(now); + } catch (IOException ex) { + LogHelper.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to check VIP", ex); // should never happen + } + }); } // helpers - private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException { - return Requester.getConnectionFromRoute(SettingsEnum.SB_API_URL.getString(), route, params); + private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException { + HttpURLConnection connection = Requester.getConnectionFromRoute(SettingsEnum.SB_API_URL.getString(), route, params); + connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS); + connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS); + return connection; } - private static JSONObject getJSONObject(Route route, String... params) throws Exception { - return Requester.parseJSONObjectAndDisconnect(getConnectionFromRoute(route, params)); + private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException { + return Requester.parseJSONObject(getConnectionFromRoute(route, params)); } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java index 7926b76d..6bf90a94 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRoutes.java @@ -9,11 +9,11 @@ class SBRoutes { static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}"); static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}"); static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}"); - static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]"); + static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"viewCount\",\"minutesSaved\"]"); static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}"); - static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}&videoDuration={duration}"); - static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}"); - static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}"); + static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?userID={user_id}&videoID={video_id}&category={category}&startTime={start_time}&endTime={end_time}&videoDuration={duration}"); + static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}"); + static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}"); private SBRoutes() { } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java new file mode 100644 index 00000000..432cf4eb --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/CreateSegmentButtonController.java @@ -0,0 +1,124 @@ +package app.revanced.integrations.sponsorblock.ui; + +import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; + +import android.view.View; +import android.view.animation.Animation; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import java.lang.ref.WeakReference; + +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public class CreateSegmentButtonController { + private static WeakReference buttonReference = new WeakReference<>(null); + private static Animation fadeIn; + private static Animation fadeOut; + private static boolean isShowing; + + /** + * injection point + */ + public static void initialize(Object viewStub) { + try { + LogHelper.printDebug(() -> "initializing new segment button"); + + RelativeLayout youtubeControlsLayout = (RelativeLayout) viewStub; + String buttonIdentifier = "sb_sponsorblock_button"; + ImageView imageView = youtubeControlsLayout.findViewById(getResourceIdentifier(buttonIdentifier, "id")); + if (imageView == null) { + LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonIdentifier + "\""); + return; + } + imageView.setOnClickListener(v -> { + LogHelper.printDebug(() -> "New segment button clicked"); + SponsorBlockViewController.toggleNewSegmentLayoutVisibility(); + }); + buttonReference = new WeakReference<>(imageView); + + // Animations + if (fadeIn == null) { + fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); + fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast")); + fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); + fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled")); + } + isShowing = true; + changeVisibilityImmediate(false); + } catch (Exception ex) { + LogHelper.printException(() -> "initialize failure", ex); + } + } + + public static void changeVisibilityImmediate(boolean visible) { + changeVisibility(visible, true); + } + + /** + * injection point + */ + public static void changeVisibilityNegatedImmediate(boolean visible) { + changeVisibility(!visible, true); + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible) { + changeVisibility(visible, false); + } + + public static void changeVisibility(boolean visible, boolean immediate) { + try { + if (isShowing == visible) return; + isShowing = visible; + + ImageView iView = buttonReference.get(); + if (iView == null) return; + + if (visible) { + iView.clearAnimation(); + if (!shouldBeShown()) { + return; + } + if (!immediate) { + iView.startAnimation(fadeIn); + } + iView.setVisibility(View.VISIBLE); + return; + } + + if (iView.getVisibility() == View.VISIBLE) { + iView.clearAnimation(); + if (!immediate) { + iView.startAnimation(fadeOut); + } + iView.setVisibility(View.GONE); + } + } catch (Exception ex) { + LogHelper.printException(() -> "changeVisibility failure", ex); + } + } + + private static boolean shouldBeShown() { + return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean() + && !VideoInformation.isAtEndOfVideo(); + } + + public static void hide() { + if (!isShowing) { + return; + } + ReVancedUtils.verifyOnMainThread(); + View v = buttonReference.get(); + if (v == null) { + return; + } + v.setVisibility(View.GONE); + isShowing = false; + } +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java new file mode 100644 index 00000000..88ed535b --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/NewSegmentLayout.java @@ -0,0 +1,124 @@ +package app.revanced.integrations.sponsorblock.ui; + +import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize; +import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.ImageButton; + +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.sponsorblock.SponsorBlockUtils; +import app.revanced.integrations.utils.LogHelper; + +public class NewSegmentLayout extends FrameLayout { + private final int rippleEffectId; + final int defaultBottomMargin; + final int ctaBottomMargin; + + public NewSegmentLayout(Context context) { + this(context, null); + } + + public NewSegmentLayout(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr) { + this(context, attributeSet, defStyleAttr, 0); + } + + public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { + super(context, attributeSet, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate(getResourceIdentifier(context, "new_segment", "layout"), this, true); + + TypedValue rippleEffect = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true); + rippleEffectId = rippleEffect.resourceId; + + // LinearLayout newSegmentContainer = findViewById(getResourceIdentifier(context, "sb_new_segment_container", "id")); + + ImageButton rewindButton = findViewById(getResourceIdentifier(context, "sb_new_segment_rewind", "id")); + if (rewindButton == null) { + LogHelper.printException(() -> "Could not find rewindButton"); + } else { + setClickEffect(rewindButton); + rewindButton.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Rewind button clicked"); + VideoInformation.seekToRelative(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()); + }); + } + ImageButton forwardButton = findViewById(getResourceIdentifier(context, "sb_new_segment_forward", "id")); + if (forwardButton == null) { + LogHelper.printException(() -> "Could not find forwardButton"); + } else { + setClickEffect(forwardButton); + forwardButton.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Forward button clicked"); + VideoInformation.seekToRelative(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()); + }); + } + ImageButton adjustButton = findViewById(getResourceIdentifier(context, "sb_new_segment_adjust", "id")); + if (adjustButton == null) { + LogHelper.printException(() -> "Could not find adjustButton"); + } else { + setClickEffect(adjustButton); + adjustButton.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Adjust button clicked"); + SponsorBlockUtils.onMarkLocationClicked(); + }); + } + ImageButton compareButton = findViewById(getResourceIdentifier(context, "sb_new_segment_compare", "id")); + if (compareButton == null) { + LogHelper.printException(() -> "Could not find compareButton"); + } else { + setClickEffect(compareButton); + compareButton.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Compare button clicked"); + SponsorBlockUtils.onPreviewClicked(); + }); + } + ImageButton editButton = findViewById(getResourceIdentifier(context, "sb_new_segment_edit", "id")); + if (editButton == null) { + LogHelper.printException(() -> "Could not find editButton"); + } else { + setClickEffect(editButton); + editButton.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Edit button clicked"); + SponsorBlockUtils.onEditByHandClicked(); + }); + } + ImageButton publishButton = findViewById(getResourceIdentifier(context, "sb_new_segment_publish", "id")); + if (publishButton == null) { + LogHelper.printException(() -> "Could not find publishButton"); + } else { + setClickEffect(publishButton); + publishButton.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Publish button clicked"); + SponsorBlockUtils.onPublishClicked(); + }); + } + + defaultBottomMargin = getResourceDimensionPixelSize("brand_interaction_default_bottom_margin"); + ctaBottomMargin = getResourceDimensionPixelSize("brand_interaction_cta_bottom_margin"); + } + + private void setClickEffect(ImageButton btn) { + btn.setBackgroundResource(rippleEffectId); + + RippleDrawable rippleDrawable = (RippleDrawable) btn.getBackground(); + + int[][] states = new int[][]{new int[]{android.R.attr.state_enabled}}; + int[] colors = new int[]{0x33ffffff}; // sets the ripple color to white + + ColorStateList colorStateList = new ColorStateList(states, colors); + rippleDrawable.setColor(colorStateList); + } +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java new file mode 100644 index 00000000..588abd98 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java @@ -0,0 +1,103 @@ +package app.revanced.integrations.sponsorblock.ui; + +import static app.revanced.integrations.utils.ReVancedUtils.getResourceColor; +import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimension; +import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize; +import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; +import static app.revanced.integrations.utils.StringRef.str; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.Objects; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.sponsorblock.SegmentPlaybackController; +import app.revanced.integrations.sponsorblock.objects.SponsorSegment; +import app.revanced.integrations.utils.LogHelper; + +public class SkipSponsorButton extends FrameLayout { + private static final boolean highContrast = true; + private final LinearLayout skipSponsorBtnContainer; + private final TextView skipSponsorTextView; + private final CharSequence skipSponsorTextCompact; + private final Paint background; + private final Paint border; + final int defaultBottomMargin; + final int ctaBottomMargin; + + public SkipSponsorButton(Context context) { + this(context, null); + } + + public SkipSponsorButton(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) { + this(context, attributeSet, defStyleAttr, 0); + } + + public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { + super(context, attributeSet, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate(getResourceIdentifier(context, "skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button + setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height + skipSponsorBtnContainer = Objects.requireNonNull((LinearLayout) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container + background = new Paint(); + background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color); + background.setStyle(Paint.Style.FILL); + border = new Paint(); + border.setColor(getResourceColor("skip_ad_button_border_color")); // color:skip_ad_button_border_color); + border.setStrokeWidth(getResourceDimension("ad_skip_ad_button_border_width")); // dimen:ad_skip_ad_button_border_width); + border.setStyle(Paint.Style.STROKE); + skipSponsorTextView = Objects.requireNonNull((TextView) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_text", "id"))); // id:skip_ad_button_text; + defaultBottomMargin = getResourceDimensionPixelSize("skip_button_default_bottom_margin"); // dimen:skip_button_default_bottom_margin + ctaBottomMargin = getResourceDimensionPixelSize("skip_button_cta_bottom_margin"); // dimen:skip_button_cta_bottom_margin + skipSponsorTextCompact = str("sb_skip_button_compact"); // string:skip_ads "Skip ads" + + skipSponsorBtnContainer.setOnClickListener(v -> { + LogHelper.printDebug(() -> "Skip button clicked"); + SegmentPlaybackController.onSkipSponsorClicked(); + }); + } + + @Override // android.view.ViewGroup + protected final void dispatchDraw(Canvas canvas) { + final int left = skipSponsorBtnContainer.getLeft(); + final int top = skipSponsorBtnContainer.getTop(); + final int leftPlusWidth = (left + skipSponsorBtnContainer.getWidth()); + final int topPlusHeight = (top + skipSponsorBtnContainer.getHeight()); + canvas.drawRect(left, top, leftPlusWidth, topPlusHeight, background); + if (!highContrast) { + canvas.drawLines(new float[]{ + leftPlusWidth, top, left, top, + left, top, left, topPlusHeight, + left, topPlusHeight, leftPlusWidth, topPlusHeight}, + border); + } + + super.dispatchDraw(canvas); + } + + /** + * @return true, if this button state was changed + */ + public boolean updateSkipButtonText(SponsorSegment segment) { + CharSequence newText = SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean() + ? skipSponsorTextCompact + : segment.getSkipButtonText(); + if (newText.equals(skipSponsorTextView.getText())) { + return false; + } + skipSponsorTextView.setText(newText); + return true; + } +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java new file mode 100644 index 00000000..834dbc2f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java @@ -0,0 +1,228 @@ +package app.revanced.integrations.sponsorblock.ui; + +import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.shared.PlayerType; +import app.revanced.integrations.sponsorblock.objects.SponsorSegment; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public class SponsorBlockViewController { + private static WeakReference inlineSponsorOverlayRef = new WeakReference<>(null); + private static WeakReference youtubeOverlaysLayoutRef = new WeakReference<>(null); + private static WeakReference skipSponsorButtonRef = new WeakReference<>(null); + private static WeakReference newSegmentLayoutRef = new WeakReference<>(null); + private static boolean canShowViewElements = true; + @Nullable + private static SponsorSegment skipSegment; + + static { + PlayerType.getOnChange().addObserver((PlayerType type) -> { + playerTypeChanged(type); + return null; + }); + } + + public static Context getOverLaysViewGroupContext() { + ViewGroup group = youtubeOverlaysLayoutRef.get(); + if (group == null) { + return null; + } + return group.getContext(); + } + + /** + * Injection point. + */ + public static void initialize(Object obj) { + try { + LogHelper.printDebug(() -> "initializing"); + + RelativeLayout layout = new RelativeLayout(ReVancedUtils.getContext()); + layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT)); + LayoutInflater.from(ReVancedUtils.getContext()).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout); + inlineSponsorOverlayRef = new WeakReference<>(layout); + + ViewGroup viewGroup = (ViewGroup) obj; + viewGroup.addView(layout, viewGroup.getChildCount() - 2); + youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup); + + skipSponsorButtonRef = new WeakReference<>( + Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_sponsor_button", "id")))); + + newSegmentLayoutRef = new WeakReference<>( + Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_new_segment_view", "id")))); + } catch (Exception ex) { + LogHelper.printException(() -> "initialize failure", ex); + } + } + + public static void showSkipButton(@NonNull SponsorSegment info) { + skipSegment = Objects.requireNonNull(info); + updateSkipButton(); + } + + public static void hideSkipButton() { + skipSegment = null; + updateSkipButton(); + } + + private static void updateSkipButton() { + SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); + if (skipSponsorButton == null) { + return; + } + if (skipSegment == null) { + setSkipSponsorButtonVisibility(false); + } else { + final boolean layoutNeedsUpdating = skipSponsorButton.updateSkipButtonText(skipSegment); + if (layoutNeedsUpdating) { + bringLayoutToFront(); + } + setSkipSponsorButtonVisibility(true); + } + } + + public static void showNewSegmentLayout() { + setNewSegmentLayoutVisibility(true); + } + + public static void hideNewSegmentLayout() { + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + if (newSegmentLayout == null) { + return; + } + setNewSegmentLayoutVisibility(false); + } + + public static void toggleNewSegmentLayoutVisibility() { + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + if (newSegmentLayout == null) { + LogHelper.printException(() -> "toggleNewSegmentLayoutVisibility failure"); + return; + } + setNewSegmentLayoutVisibility(newSegmentLayout.getVisibility() == View.VISIBLE ? false : true); + } + + private static void playerTypeChanged(PlayerType playerType) { + try { + final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN; + canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED); + + setSkipButtonMargins(isWatchFullScreen); + setNewSegmentLayoutMargins(isWatchFullScreen); + updateSkipButton(); + } catch (Exception ex) { + LogHelper.printException(() -> "Player type changed error", ex); + } + } + + private static void setSkipButtonMargins(boolean fullScreen) { + SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); + if (skipSponsorButton == null) { + LogHelper.printException(() -> "setSkipButtonMargins failure"); + return; + } + + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams(); + if (params == null) { + LogHelper.printException(() -> "setSkipButtonMargins failure"); + return; + } + params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin; + skipSponsorButton.setLayoutParams(params); + } + + private static void setSkipSponsorButtonVisibility(boolean visible) { + SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); + if (skipSponsorButton == null) { + LogHelper.printException(() -> "setSkipSponsorButtonVisibility failure"); + return; + } + + visible &= canShowViewElements; + + final int desiredVisibility = visible ? View.VISIBLE : View.GONE; + if (skipSponsorButton.getVisibility() != desiredVisibility) { + skipSponsorButton.setVisibility(desiredVisibility); + if (visible) { + bringLayoutToFront(); + } + } + } + + private static void setNewSegmentLayoutMargins(boolean fullScreen) { + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + if (newSegmentLayout == null) { + LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (button is null)"); + return; + } + + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams(); + if (params == null) { + LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)"); + return; + } + params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin; + newSegmentLayout.setLayoutParams(params); + } + + private static void setNewSegmentLayoutVisibility(boolean visible) { + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + if (newSegmentLayout == null) { + LogHelper.printException(() -> "setNewSegmentLayoutVisibility failure"); + return; + } + + visible &= canShowViewElements; + + final int desiredVisibility = visible ? View.VISIBLE : View.GONE; + if (newSegmentLayout.getVisibility() != desiredVisibility) { + newSegmentLayout.setVisibility(desiredVisibility); + if (visible) { + bringLayoutToFront(); + } + } + } + + private static void bringLayoutToFront() { + RelativeLayout layout = inlineSponsorOverlayRef.get(); + if (layout != null) { + // needed to keep skip button overtop end screen cards + layout.bringToFront(); + layout.requestLayout(); + layout.invalidate(); + } + } + + /** + * Injection point. + */ + public static void endOfVideoReached() { + try { + LogHelper.printDebug(() -> "endOfVideoReached"); + // the buttons automatically set themselves to visible when appropriate, + // but if buttons are showing when the end of the video is reached then they need + // to be forcefully hidden + if (!SettingsEnum.PREFERRED_AUTO_REPEAT.getBoolean()) { + CreateSegmentButtonController.hide(); + VotingButtonController.hide(); + } + } catch (Exception ex) { + LogHelper.printException(() -> "endOfVideoReached failure", ex); + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java new file mode 100644 index 00000000..4a5a162d --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/VotingButtonController.java @@ -0,0 +1,125 @@ +package app.revanced.integrations.sponsorblock.ui; + +import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; + +import android.view.View; +import android.view.animation.Animation; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import java.lang.ref.WeakReference; + +import app.revanced.integrations.patches.VideoInformation; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.sponsorblock.SegmentPlaybackController; +import app.revanced.integrations.sponsorblock.SponsorBlockUtils; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public class VotingButtonController { + private static WeakReference buttonReference = new WeakReference<>(null); + private static Animation fadeIn; + private static Animation fadeOut; + private static boolean isShowing; + + /** + * injection point + */ + public static void initialize(Object viewStub) { + try { + LogHelper.printDebug(() -> "initializing voting button"); + RelativeLayout controlsLayout = (RelativeLayout) viewStub; + String buttonResourceName = "sb_voting_button"; + ImageView imageView = controlsLayout.findViewById(getResourceIdentifier(buttonResourceName, "id")); + if (imageView == null) { + LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonResourceName + "\""); + return; + } + imageView.setOnClickListener(v -> { + SponsorBlockUtils.onVotingClicked(v.getContext()); + }); + buttonReference = new WeakReference<>(imageView); + + // Animations + if (fadeIn == null) { + fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); + fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast")); + fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); + fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled")); + } + isShowing = true; + changeVisibilityImmediate(false); + } catch (Exception ex) { + LogHelper.printException(() -> "Unable to set RelativeLayout", ex); + } + } + + public static void changeVisibilityImmediate(boolean visible) { + changeVisibility(visible, true); + } + + /** + * injection point + */ + public static void changeVisibilityNegatedImmediate(boolean visible) { + changeVisibility(!visible, true); + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible) { + changeVisibility(visible, false); + } + + public static void changeVisibility(boolean visible, boolean immediate) { + try { + if (isShowing == visible) return; + isShowing = visible; + + ImageView iView = buttonReference.get(); + if (iView == null) return; + + if (visible) { + iView.clearAnimation(); + if (!shouldBeShown()) { + return; + } + if (!immediate) { + iView.startAnimation(fadeIn); + } + iView.setVisibility(View.VISIBLE); + return; + } + + if (iView.getVisibility() == View.VISIBLE) { + iView.clearAnimation(); + if (!immediate) { + iView.startAnimation(fadeOut); + } + iView.setVisibility(View.GONE); + } + } catch (Exception ex) { + LogHelper.printException(() -> "changeVisibility failure", ex); + } + } + + private static boolean shouldBeShown() { + return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean() + && SegmentPlaybackController.currentVideoHasSegments() && !VideoInformation.isAtEndOfVideo(); + } + + public static void hide() { + if (!isShowing) { + return; + } + ReVancedUtils.verifyOnMainThread(); + View v = buttonReference.get(); + if (v == null) { + LogHelper.printDebug(() -> "Cannot hide voting button (value is null)"); + return; + } + v.setVisibility(View.GONE); + isShowing = false; + } +} diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt index e6b167b1..4213102e 100644 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/SwipeControlsHostActivity.kt @@ -16,8 +16,8 @@ import app.revanced.integrations.swipecontrols.controller.gesture.PressToSwipeCo import app.revanced.integrations.swipecontrols.controller.gesture.core.GestureController import app.revanced.integrations.swipecontrols.misc.Rectangle import app.revanced.integrations.swipecontrols.views.SwipeControlsOverlayLayout +import app.revanced.integrations.utils.LogHelper.printDebug import app.revanced.integrations.utils.LogHelper.printException -import app.revanced.integrations.utils.LogHelper.printInfo import java.lang.ref.WeakReference /** @@ -121,7 +121,7 @@ class SwipeControlsHostActivity : Activity() { */ private fun initialize() { // create controllers - printInfo { "initializing swipe controls controllers" } + printDebug { "initializing swipe controls controllers" } config = SwipeControlsConfigurationProvider(this) keys = VolumeKeysController(this) audio = createAudioController() @@ -157,7 +157,7 @@ class SwipeControlsHostActivity : Activity() { * (re) attaches swipe overlays */ private fun reAttachOverlays() { - printInfo { "attaching swipe controls overlay" } + printDebug{ "attaching swipe controls overlay" } contentRoot.removeView(overlay) contentRoot.addView(overlay) } diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/SwipeZonesController.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/SwipeZonesController.kt index f6436e80..efa05429 100644 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/controller/SwipeZonesController.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/controller/SwipeZonesController.kt @@ -56,7 +56,7 @@ class SwipeZonesController( /** * id for R.id.player_view */ - private val playerViewId = ReVancedUtils.getResourceIdByName(host, "id", "player_view") + private val playerViewId = ReVancedUtils.getResourceIdentifier(host, "player_view", "id") /** * current bounding rectangle of the player diff --git a/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt index f27d0366..b80ed822 100644 --- a/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt +++ b/app/src/main/java/app/revanced/integrations/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -39,7 +39,7 @@ class SwipeControlsOverlayLayout( private fun getDrawable(name: String, width: Int, height: Int): Drawable { return resources.getDrawable( - ReVancedUtils.getResourceIdByName(context, "drawable", name), + ReVancedUtils.getResourceIdentifier(context, name, "drawable"), context.theme ).apply { setTint(config.overlayForegroundColor) diff --git a/app/src/main/java/app/revanced/integrations/utils/Event.kt b/app/src/main/java/app/revanced/integrations/utils/Event.kt index f2fa21c3..7f7b1449 100644 --- a/app/src/main/java/app/revanced/integrations/utils/Event.kt +++ b/app/src/main/java/app/revanced/integrations/utils/Event.kt @@ -7,10 +7,18 @@ class Event { private val eventListeners = mutableSetOf<(T) -> Unit>() operator fun plusAssign(observer: (T) -> Unit) { + addObserver(observer) + } + + fun addObserver(observer: (T) -> Unit) { eventListeners.add(observer) } operator fun minusAssign(observer: (T) -> Unit) { + removeObserver(observer) + } + + fun removeObserver(observer: (T) -> Unit) { eventListeners.remove(observer) } diff --git a/app/src/main/java/app/revanced/integrations/utils/LogHelper.java b/app/src/main/java/app/revanced/integrations/utils/LogHelper.java index aa7746a8..19e3c127 100644 --- a/app/src/main/java/app/revanced/integrations/utils/LogHelper.java +++ b/app/src/main/java/app/revanced/integrations/utils/LogHelper.java @@ -1,8 +1,6 @@ package app.revanced.integrations.utils; -import android.content.Context; import android.util.Log; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -131,12 +129,7 @@ public class LogHelper { String toastMessageToDisplay = (userToastMessage != null) ? userToastMessage : outerClassSimpleName + ": " + messageString; - ReVancedUtils.runOnMainThread(() -> { - Context context = ReVancedUtils.getContext(); - if (context != null) { - Toast.makeText(context, toastMessageToDisplay, Toast.LENGTH_LONG).show(); - } - }); + ReVancedUtils.showToastLong(toastMessageToDisplay); } } diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index 1895ad98..b92d4bd5 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -3,134 +3,119 @@ package app.revanced.integrations.utils; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; +import android.net.ConnectivityManager; import android.os.Handler; import android.os.Looper; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.text.Bidi; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import app.revanced.integrations.sponsorblock.player.PlayerType; - public class ReVancedUtils { - private static PlayerType env; - private static boolean newVideo = false; - @SuppressLint("StaticFieldLeak") public static Context context; private ReVancedUtils() { } // utility class - /** - * Maximum number of background threads run concurrently - */ - private static final int SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS = 20; - /** * General purpose pool for network calls and other background tasks. * All tasks run at max thread priority. */ private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor( - 2, // minimum 2 threads always ready to be used + 2, // 2 threads always ready to go + Integer.MAX_VALUE, 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle - SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS, TimeUnit.SECONDS, - new LinkedBlockingQueue(), - new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MAX_PRIORITY); // run at max priority - return t; - } + new SynchronousQueue<>(), + r -> { // ThreadFactory + Thread t = new Thread(r); + t.setPriority(Thread.MAX_PRIORITY); // run at max priority + return t; }); - private static void checkIfPoolHasReachedLimit() { - if (backgroundThreadPool.getActiveCount() >= SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS) { - // Something is wrong. Background threads are piling up and not completing as expected, - // or some ReVanced code is submitting an unexpected number of background tasks. - LogHelper.printException(() -> "Reached maximum background thread count of " - + SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS + " threads"); - } - } - - public static void runOnBackgroundThread(Runnable task) { + public static void runOnBackgroundThread(@NonNull Runnable task) { backgroundThreadPool.execute(task); - checkIfPoolHasReachedLimit(); } - public static Future submitOnBackgroundThread(Callable call) { - Future future = backgroundThreadPool.submit(call); - checkIfPoolHasReachedLimit(); - return future; + @NonNull + public static Future submitOnBackgroundThread(@NonNull Callable call) { + return backgroundThreadPool.submit(call); } - public static boolean containsAny(final String value, final String... targets) { + public static boolean containsAny(@NonNull String value, @NonNull String... targets) { for (String string : targets) if (!string.isEmpty() && value.contains(string)) return true; return false; } - public static void setNewVideo(boolean started) { - LogHelper.printDebug(() -> "New video started: " + started); - newVideo = started; + /** + * @return zero, if the resource is not found + */ + @SuppressLint("DiscouragedApi") + public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) { + return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName()); } - public static boolean isNewVideoStarted() { - return newVideo; + /** + * @return zero, if the resource is not found + */ + public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) { + return getResourceIdentifier(getContext(), resourceIdentifierName, type); } - public static Integer getResourceIdByName(Context context, String type, String name) { - try { - Resources res = context.getResources(); - return res.getIdentifier(name, type, context.getPackageName()); - } catch (Throwable exception) { - LogHelper.printException(() -> "Resource not found.", exception); - return null; - } + public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer")); } - public static void setPlayerType(PlayerType type) { - env = type; + @NonNull + public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim")); } - public static PlayerType getPlayerType() { - return env; + public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); } - public static int getIdentifier(String name, String defType) { - Context context = getContext(); - return context.getResources().getIdentifier(name, defType, context.getPackageName()); + public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen")); + } + + public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); } public static Context getContext() { if (context != null) { return context; - } else { - LogHelper.printException(() -> "Context is null, returning null!"); - return null; } + LogHelper.printException(() -> "Context is null, returning null!"); + return null; } - public static void setClipboard(String text) { + public static void setClipboard(@NonNull String text) { android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text); clipboard.setPrimaryClip(clip); } - public static boolean isTablet(Context context) { + public static boolean isTablet() { return context.getResources().getConfiguration().smallestScreenWidthDp >= 600; } + @Nullable private static Boolean isRightToLeftTextLayout; /** * If the device language uses right to left text layout (hebrew, arabic, etc) @@ -144,16 +129,46 @@ public class ReVancedUtils { } /** - * Automatically logs any exceptions the runnable throws + * Safe to call from any thread */ - public static void runOnMainThread(Runnable runnable) { + public static void showToastShort(@NonNull String messageToToast) { + showToast(messageToToast, Toast.LENGTH_SHORT); + } + + /** + * Safe to call from any thread + */ + public static void showToastLong(@NonNull String messageToToast) { + showToast(messageToToast, Toast.LENGTH_LONG); + } + + private static void showToast(@NonNull String messageToToast, int toastDuration) { + Objects.requireNonNull(messageToToast); + runOnMainThreadNowOrLater(() -> { + // cannot use getContext(), otherwise if context is null it will cause infinite recursion of error logging + if (context == null) { + LogHelper.printDebug(() -> "Cannot show toast (context is null)"); + } else { + LogHelper.printDebug(() -> "Showing toast: " + messageToToast); + Toast.makeText(context, messageToToast, toastDuration).show(); + } + } + ); + } + + /** + * Automatically logs any exceptions the runnable throws. + * + * @see #runOnMainThreadNowOrLater(Runnable) + */ + public static void runOnMainThread(@NonNull Runnable runnable) { runOnMainThreadDelayed(runnable, 0); } /** * Automatically logs any exceptions the runnable throws */ - public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) { + public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) { Runnable loggingRunnable = () -> { try { runnable.run(); @@ -164,10 +179,22 @@ public class ReVancedUtils { new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis); } + /** + * If called from the main thread, the code is run immediately.

+ * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}. + */ + public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) { + if (isCurrentlyOnMainThread()) { + runnable.run(); + } else { + runOnMainThread(runnable); + } + } + /** * @return if the calling thread is on the main thread */ - public static boolean currentlyIsOnMainThread() { + public static boolean isCurrentlyOnMainThread() { return Looper.getMainLooper().isCurrentThread(); } @@ -175,7 +202,7 @@ public class ReVancedUtils { * @throws IllegalStateException if the calling thread is _off_ the main thread */ public static void verifyOnMainThread() throws IllegalStateException { - if (!currentlyIsOnMainThread()) { + if (!isCurrentlyOnMainThread()) { throw new IllegalStateException("Must call _on_ the main thread"); } } @@ -184,8 +211,37 @@ public class ReVancedUtils { * @throws IllegalStateException if the calling thread is _on_ the main thread */ public static void verifyOffMainThread() throws IllegalStateException { - if (currentlyIsOnMainThread()) { + if (isCurrentlyOnMainThread()) { throw new IllegalStateException("Must call _off_ the main thread"); } } + + public static boolean isNetworkConnected() { + NetworkType networkType = getNetworkType(); + return networkType == NetworkType.MOBILE + || networkType == NetworkType.OTHER; + } + + @SuppressLint("MissingPermission") // permission already included in YouTube + public static NetworkType getNetworkType() { + Context networkContext = getContext(); + if (networkContext == null) { + return NetworkType.NONE; + } + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + var networkInfo = cm.getActiveNetworkInfo(); + + if (networkInfo == null || !networkInfo.isConnected()) { + return NetworkType.NONE; + } + var type = networkInfo.getType(); + return (type == ConnectivityManager.TYPE_MOBILE) + || (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER; + } + + public enum NetworkType { + NONE, + MOBILE, + OTHER, + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java b/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java index 63ed3951..d792bc07 100644 --- a/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java +++ b/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java @@ -36,7 +36,7 @@ public class SharedPrefHelper { // region Hack, unknown why required - public static Long getLong(SharedPrefNames prefName, String key, Long _default) { + public static Long getLong(SharedPrefNames prefName, String key, long _default) { SharedPreferences sharedPreferences = getPreferences(prefName); try { return Long.valueOf(sharedPreferences.getString(key, _default + "")); @@ -45,7 +45,7 @@ public class SharedPrefHelper { } } - public static Float getFloat(SharedPrefNames prefName, String key, Float _default) { + public static Float getFloat(SharedPrefNames prefName, String key, float _default) { SharedPreferences sharedPreferences = getPreferences(prefName); try { return Float.valueOf(sharedPreferences.getString(key, _default + "")); @@ -54,7 +54,7 @@ public class SharedPrefHelper { } } - public static Integer getInt(SharedPrefNames prefName, String key, Integer _default) { + public static Integer getInt(SharedPrefNames prefName, String key, int _default) { SharedPreferences sharedPreferences = getPreferences(prefName); try { return Integer.valueOf(sharedPreferences.getString(key, _default + "")); diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/StringRef.java b/app/src/main/java/app/revanced/integrations/utils/StringRef.java similarity index 79% rename from app/src/main/java/app/revanced/integrations/sponsorblock/StringRef.java rename to app/src/main/java/app/revanced/integrations/utils/StringRef.java index 437290a2..e30a1d7d 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/StringRef.java +++ b/app/src/main/java/app/revanced/integrations/utils/StringRef.java @@ -1,4 +1,4 @@ -package app.revanced.integrations.sponsorblock; +package app.revanced.integrations.utils; import android.content.Context; import android.content.res.Resources; @@ -9,26 +9,22 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -// should probably move this class into utils package public class StringRef { private static Resources resources; private static String packageName; // must use a thread safe map, as this class is used both on and off the main thread - private static final Map strings = Collections.synchronizedMap(new HashMap()); + private static final Map strings = Collections.synchronizedMap(new HashMap<>()); /** - * Gets strings reference from shared collection or creates if not exists yet, - * this method should be called if you want to get StringRef + * Returns a cached instance. + * Should be used if the same String could be loaded more than once. * * @param id string resource name/id - * @return String reference that'll resolve to excepted string, may be from cache + * @see #sf(String) */ @NonNull - public static StringRef sf(@NonNull String id) { + public static StringRef sfc(@NonNull String id) { StringRef ref = strings.get(id); if (ref == null) { ref = new StringRef(id); @@ -38,18 +34,30 @@ public class StringRef { } /** - * Gets string value by string id, shorthand for sf(id).toString() + * Creates a new instance, but does not cache the value. + * Should be used for Strings that are loaded exactly once. + * + * @param id string resource name/id + * @see #sfc(String) + */ + @NonNull + public static StringRef sf(@NonNull String id) { + return new StringRef(id); + } + + /** + * Gets string value by string id, shorthand for sfc(id).toString() * * @param id string resource name/id * @return String value from string.xml */ @NonNull public static String str(@NonNull String id) { - return sf(id).toString(); + return sfc(id).toString(); } /** - * Gets string value by string id, shorthand for sf(id).toString() and formats the string + * Gets string value by string id, shorthand for sfc(id).toString() and formats the string * with given args. * * @param id string resource name/id @@ -61,7 +69,6 @@ public class StringRef { return String.format(str(id), args); } - /** * Creates a StringRef object that'll not change it's value * diff --git a/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java b/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java index 3623dc47..cbb10394 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java +++ b/app/src/main/java/app/revanced/integrations/utils/ThemeHelper.java @@ -4,13 +4,18 @@ public class ThemeHelper { private static int themeValue; public static void setTheme(int value) { - themeValue = value; - LogHelper.printDebug(() -> "Theme value: " + themeValue); + if (themeValue != value) { + themeValue = value; + LogHelper.printDebug(() -> "Theme value: " + themeValue); + } } public static void setTheme(Object value) { - themeValue = ((Enum) value).ordinal(); - LogHelper.printDebug(() -> "Theme value: " + themeValue); + final int newOrdinalValue = ((Enum) value).ordinal(); + if (themeValue != newOrdinalValue) { + themeValue = newOrdinalValue; + LogHelper.printDebug(() -> "Theme value: " + themeValue); + } } public static boolean isDarkTheme() { diff --git a/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java b/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java index 3c9190f1..60923d9f 100644 --- a/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java +++ b/app/src/main/java/app/revanced/integrations/videoplayer/BottomControlButton.java @@ -3,7 +3,6 @@ package app.revanced.integrations.videoplayer; import android.support.constraint.ConstraintLayout; import android.view.View; import android.view.animation.Animation; -import android.view.animation.AnimationUtils; import android.widget.ImageView; import java.lang.ref.WeakReference; @@ -26,22 +25,19 @@ public abstract class BottomControlButton { constraintLayout = (ConstraintLayout) obj; isButtonEnabled = isEnabled; - ImageView imageView = constraintLayout.findViewById(ReVancedUtils.getIdentifier(viewId, "id")); + ImageView imageView = constraintLayout.findViewById(ReVancedUtils.getResourceIdentifier(viewId, "id")); if (imageView == null) { - LogHelper.printDebug(() -> "Couldn't find ImageView with id: " + viewId); + LogHelper.printException(() -> "Couldn't find ImageView with id: " + viewId); return; } - imageView.setOnClickListener(onClickListener); - button = new WeakReference<>(imageView); - fadeIn = getAnimation("fade_in"); - fadeOut = getAnimation("fade_out"); - int fadeDurationFast = getInteger("fade_duration_fast"); - int fadeDurationScheduled = getInteger("fade_duration_scheduled"); - fadeIn.setDuration(fadeDurationFast); - fadeOut.setDuration(fadeDurationScheduled); + fadeIn = ReVancedUtils.getResourceAnimation("fade_in"); + fadeOut = ReVancedUtils.getResourceAnimation("fade_out"); + fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast")); + fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled")); + isShowing = true; setVisibility(false); } catch (Exception e) { @@ -69,11 +65,4 @@ public abstract class BottomControlButton { imageView.setVisibility(View.GONE); } } - private static int getInteger(String str) { - return ReVancedUtils.getContext().getResources().getInteger(ReVancedUtils.getIdentifier(str, "integer")); - } - - private static Animation getAnimation(String str) { - return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), ReVancedUtils.getIdentifier(str, "anim")); - } } diff --git a/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java b/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java index b3b8cdb5..5928e021 100644 --- a/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java +++ b/app/src/main/java/app/revanced/integrations/videoplayer/DownloadButton.java @@ -3,12 +3,12 @@ package app.revanced.integrations.videoplayer; import android.content.Intent; import android.content.pm.PackageManager; import android.view.View; -import android.widget.Toast; import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.sponsorblock.StringRef; import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import app.revanced.integrations.utils.StringRef; public class DownloadButton extends BottomControlButton { public static DownloadButton instance; @@ -46,7 +46,7 @@ public class DownloadButton extends BottomControlButton { // If the package is not installed, show the toast if (!packageEnabled) { - Toast.makeText(context, downloaderPackageName + " " + StringRef.str("downloader_not_installed_warning"), Toast.LENGTH_LONG).show(); + ReVancedUtils.showToastLong(downloaderPackageName + " " + StringRef.str("downloader_not_installed_warning")); return; } From 3025103014a4521a437cfde0a417535e7751b517 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 2 Apr 2023 22:46:25 +0400 Subject: [PATCH 04/30] fix(youtube/sponsorblock): update HTTP user agent (#344) --- .../java/app/revanced/integrations/requests/Requester.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/requests/Requester.java b/app/src/main/java/app/revanced/integrations/requests/Requester.java index 37cc6e50..55b777d9 100644 --- a/app/src/main/java/app/revanced/integrations/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/requests/Requester.java @@ -19,8 +19,7 @@ public class Requester { String url = apiUrl + route.compile(params).getCompiledRoute(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(route.getMethod().name()); - // TODO: change the user agent string - connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); + connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";revanced"); return connection; } From bbb07ec9c840fad0c282093340fc5bdd13e5ec5f Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 2 Apr 2023 21:09:52 +0200 Subject: [PATCH 05/30] ci: use correct checkmark syntax in PR body --- .github/workflows/pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index dc4a07e8..fbee1c79 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,5 +26,5 @@ jobs: ## Dependencies before merge - - [] https://github.com/revanced/revanced-patches - pr_draft: true \ No newline at end of file + - [ ] https://github.com/revanced/revanced-patches + pr_draft: true From 22e453706d6df34e8b9402eb2f579f983f8f9885 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 3 Apr 2023 03:04:04 +0200 Subject: [PATCH 06/30] chore: bump dependencies --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dfdf8d89..047e7975 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,7 +43,7 @@ android { dependencies { compileOnly(project(mapOf("path" to ":dummy"))) - compileOnly("androidx.annotation:annotation:1.5.0") + compileOnly("androidx.annotation:annotation:1.6.0") compileOnly("androidx.appcompat:appcompat:1.6.1") compileOnly("com.squareup.okhttp3:okhttp:5.0.0-alpha.11") compileOnly("com.squareup.retrofit2:retrofit:2.9.0") From d4de3f6819e76b0dfa9af6d597b3f8ec5efa8531 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:00:16 +0400 Subject: [PATCH 07/30] refactor(youtube/video-information): include video speed (#345) --- .../patches/VideoInformation.java | 37 +++++++++++++ .../speed/RememberPlaybackSpeedPatch.java | 52 +++++-------------- .../SegmentPlaybackController.java | 3 +- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java index 21299163..ccdde90d 100644 --- a/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/patches/VideoInformation.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; @@ -12,6 +13,7 @@ import app.revanced.integrations.utils.ReVancedUtils; * Hooking class for the current playing video. */ public final class VideoInformation { + private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final String SEEK_METHOD_NAME = "seekTo"; private static WeakReference playerController; @@ -21,6 +23,10 @@ public final class VideoInformation { private static String videoId = ""; private static long videoLength = 0; private static volatile long videoTime = -1; // must be volatile. Value is set off main thread from high precision patch hook + /** + * The current playback speed + */ + private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; /** * Injection point. @@ -50,6 +56,30 @@ public final class VideoInformation { if (!videoId.equals(newlyLoadedVideoId)) { LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId); videoId = newlyLoadedVideoId; + playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + } + } + + /** + * Injection point. + * Called when user selects a playback speed. + * + * @param userSelectedPlaybackSpeed The playback speed the user selected + */ + public static void userSelectedPlaybackSpeed(float userSelectedPlaybackSpeed) { + LogHelper.printDebug(() -> "User selected playback speed: " + userSelectedPlaybackSpeed); + playbackSpeed = userSelectedPlaybackSpeed; + } + + /** + * Overrides the current playback speed. + * + * Used exclusively by {@link RememberPlaybackSpeedPatch} + */ + public static void overridePlaybackSpeed(float speedOverride) { + if (playbackSpeed != speedOverride) { + LogHelper.printDebug(() -> "Overriding playback speed to: " + speedOverride); + playbackSpeed = speedOverride; } } @@ -115,6 +145,13 @@ public final class VideoInformation { return videoId; } + /** + * @return The current playback speed. + */ + public static float getCurrentPlaybackSpeed() { + return playbackSpeed; + } + /** * Length of the current video playing. * Includes Shorts playback. diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java index 8b4b2114..babbc291 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -3,34 +3,15 @@ package app.revanced.integrations.patches.playback.speed; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import app.revanced.integrations.patches.VideoInformation; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public final class RememberPlaybackSpeedPatch { - /** - * The current playback speed - */ - private static float currentPlaybackSpeed = getLastRememberedPlaybackSpeed(); - - private final static float DEFAULT_PLAYBACK_SPEED = (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getDefaultValue(); - @Nullable private static String currentVideoId; - private static float getLastRememberedPlaybackSpeed() { - return SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat(); - } - - private static void rememberPlaybackSpeed() { - SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.saveValue(currentPlaybackSpeed); - } - - private static boolean rememberLastSelectedPlaybackSpeed() { - return SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean(); - } - /** * Injection point. * Called when a new video loads. @@ -39,41 +20,32 @@ public final class RememberPlaybackSpeedPatch { if (videoId.equals(currentVideoId)) { return; } - currentVideoId = videoId; - currentPlaybackSpeed = getLastRememberedPlaybackSpeed(); + VideoInformation.overridePlaybackSpeed(SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat()); } /** * Injection point. - * Called when a playback speed is selected. + * Called when user selects a playback speed. * - * @param playbackSpeed The playback speed to set. + * @param playbackSpeed The playback speed the user selected */ - public static void setPlaybackSpeed(final float playbackSpeed) { - LogHelper.printDebug(() -> "Playback speed changed to: " + playbackSpeed); - - currentPlaybackSpeed = playbackSpeed; - - if (rememberLastSelectedPlaybackSpeed()) { - rememberPlaybackSpeed(); + public static void userSelectedPlaybackSpeed(float playbackSpeed) { + if (SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean()) { + SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.saveValue(playbackSpeed); + // TODO: extract these strings into localized file ReVancedUtils.showToastLong("Remembering playback speed: " + playbackSpeed + "x"); - } else { - if (getLastRememberedPlaybackSpeed() == DEFAULT_PLAYBACK_SPEED) return; - + } else if (playbackSpeed != (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getDefaultValue()) { ReVancedUtils.showToastLong("Applying playback speed: " + playbackSpeed + "x"); } } /** * Injection point. - * Called when playback first starts, - * and also called immediately after the user selects a new video speed. - * - * @return The currently set playback speed. + * Overrides the video speed. Called after video loads, and immediately after user selects a different playback speed */ - public static float getCurrentPlaybackSpeed() { - return currentPlaybackSpeed; + public static float getPlaybackSpeedOverride() { + return VideoInformation.getCurrentPlaybackSpeed(); } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java index 5435bfe0..d2cea53d 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java @@ -14,7 +14,6 @@ import java.util.Arrays; import java.util.Objects; import app.revanced.integrations.patches.VideoInformation; -import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.shared.PlayerType; import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; @@ -189,7 +188,7 @@ public class SegmentPlaybackController { // to debug the timing logic, set this to a very large value (5000 or more) // then try manually seeking just playback reaches a skip/hide of different segments final long lookAheadMilliseconds = 1500; // must be larger than the average time between calls to this method - final float playbackSpeed = RememberPlaybackSpeedPatch.getCurrentPlaybackSpeed(); + final float playbackSpeed = VideoInformation.getCurrentPlaybackSpeed(); final long startTimerLookAheadThreshold = millis + (long)(playbackSpeed * lookAheadMilliseconds); SponsorSegment foundCurrentSegment = null; From a0ad968aaa66422e67de2a61d76bc7aa88f08bf6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 5 Apr 2023 12:24:55 +0400 Subject: [PATCH 08/30] feat(youtube/settings): disable preference control if the feature is turned off. show a dialog explaining side effects of some patches (#328) Co-authored-by: semantic-release-bot Co-authored-by: oSumAtrIX --- .../integrations/patches/HideReelsPatch.java | 20 - .../quality/RememberVideoQualityPatch.java | 7 +- .../speed/RememberPlaybackSpeedPatch.java | 2 +- .../integrations/settings/ReturnType.java | 10 - .../integrations/settings/SettingsEnum.java | 480 +++++++++++------- .../settings/SharedPrefCategory.java | 97 ++++ .../settingsmenu/ReVancedSettingActivity.java | 6 + .../ReVancedSettingsFragment.java | 190 ++++--- .../ReturnYouTubeDislikeSettingsFragment.java | 16 +- .../SponsorBlockSettingsFragment.java | 10 +- .../sponsorblock/SponsorBlockSettings.java | 4 +- .../sponsorblock/objects/SegmentCategory.java | 4 +- .../integrations/utils/SharedPrefHelper.java | 94 ---- .../revanced/tiktok/settings/ReturnType.java | 5 - .../tiktok/settings/SettingsEnum.java | 201 ++++---- .../tiktok/settings/SharedPrefCategory.java | 71 +++ .../ReVancedSettingsFragment.java | 46 +- .../tiktok/utils/SharedPrefHelper.java | 82 --- .../revanced/twitch/settings/ReturnType.java | 5 - .../twitch/settings/SettingsEnum.java | 165 +++--- .../ReVancedSettingsFragment.java | 15 +- 21 files changed, 800 insertions(+), 730 deletions(-) delete mode 100644 app/src/main/java/app/revanced/integrations/patches/HideReelsPatch.java delete mode 100644 app/src/main/java/app/revanced/integrations/settings/ReturnType.java create mode 100644 app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java delete mode 100644 app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java delete mode 100644 app/src/main/java/app/revanced/tiktok/settings/ReturnType.java create mode 100644 app/src/main/java/app/revanced/tiktok/settings/SharedPrefCategory.java delete mode 100644 app/src/main/java/app/revanced/tiktok/utils/SharedPrefHelper.java delete mode 100644 app/src/main/java/app/revanced/twitch/settings/ReturnType.java diff --git a/app/src/main/java/app/revanced/integrations/patches/HideReelsPatch.java b/app/src/main/java/app/revanced/integrations/patches/HideReelsPatch.java deleted file mode 100644 index 680d05e0..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/HideReelsPatch.java +++ /dev/null @@ -1,20 +0,0 @@ -package app.revanced.integrations.patches; - -import android.view.View; - -import app.revanced.integrations.adremover.AdRemoverAPI; -import app.revanced.integrations.settings.SettingsEnum; - -public class HideReelsPatch { - - /** - * Used by app.revanced.patches.youtube.layout.reels.patch.HideReelsPatch - * - * @param view - */ - public static void HideReel(View view) { - if (SettingsEnum.HIDE_REEL_BUTTON.getBoolean()) { - AdRemoverAPI.HideViewWithLayout1dp(view); - } - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java index a9fd837e..695b52ac 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java @@ -8,13 +8,12 @@ import java.util.ArrayList; import java.util.Collections; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.ReVancedUtils.NetworkType; -import app.revanced.integrations.utils.SharedPrefHelper; public class RememberVideoQualityPatch { - public static int selectedQuality1 = -2; private static Boolean newVideo = false; private static Boolean userChangedQuality = false; @@ -35,7 +34,7 @@ public class RememberVideoQualityPatch { preferenceKey = "mobile_quality"; } - SharedPrefHelper.saveString(SharedPrefHelper.SharedPrefNames.REVANCED_PREFS, preferenceKey, defaultQuality + ""); + SharedPrefCategory.REVANCED_PREFS.saveString(preferenceKey, String.valueOf(defaultQuality)); ReVancedUtils.showToastShort("Changing default " + networkTypeMessage + " quality to: " + defaultQuality); } @@ -94,7 +93,7 @@ public class RememberVideoQualityPatch { var preferenceKey = "wifi_quality"; if (networkType == NetworkType.MOBILE) preferenceKey = "mobile_quality"; - int preferredQuality = SharedPrefHelper.getInt(SharedPrefHelper.SharedPrefNames.REVANCED_PREFS, preferenceKey, -2); + int preferredQuality = SharedPrefCategory.REVANCED_PREFS.getInt(preferenceKey, -2); if (preferredQuality == -2) return quality; for (int streamQuality2 : iStreamQualities) { diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java index babbc291..5b4a9460 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -36,7 +36,7 @@ public final class RememberPlaybackSpeedPatch { // TODO: extract these strings into localized file ReVancedUtils.showToastLong("Remembering playback speed: " + playbackSpeed + "x"); - } else if (playbackSpeed != (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getDefaultValue()) { + } else if (playbackSpeed != (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.defaultValue) { ReVancedUtils.showToastLong("Applying playback speed: " + playbackSpeed + "x"); } } diff --git a/app/src/main/java/app/revanced/integrations/settings/ReturnType.java b/app/src/main/java/app/revanced/integrations/settings/ReturnType.java deleted file mode 100644 index df1d710a..00000000 --- a/app/src/main/java/app/revanced/integrations/settings/ReturnType.java +++ /dev/null @@ -1,10 +0,0 @@ -package app.revanced.integrations.settings; - -public enum ReturnType { - - BOOLEAN, - INTEGER, - STRING, - LONG, - FLOAT; -} diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 1f34db68..8819bd6e 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -1,190 +1,289 @@ package app.revanced.integrations.settings; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.BOOLEAN; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.FLOAT; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.INTEGER; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.LONG; +import static app.revanced.integrations.settings.SettingsEnum.ReturnType.STRING; +import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE; +import static app.revanced.integrations.settings.SharedPrefCategory.SPONSOR_BLOCK; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.integrations.utils.StringRef; public enum SettingsEnum { //Download Settings - // TODO: DOWNLOAD_PATH("revanced_download_path", Environment.getExternalStorageDirectory().getPath() + "/Download", ReturnType.STRING), - DOWNLOADS_BUTTON_SHOWN("revanced_downloads_enabled", true, ReturnType.BOOLEAN, true), - DOWNLOADS_PACKAGE_NAME("revanced_downloads_package_name", "org.schabi.newpipe" /* NewPipe */, ReturnType.STRING), + // TODO: DOWNLOAD_PATH("revanced_download_path", STRING, Environment.getExternalStorageDirectory().getPath() + "/Download"), + DOWNLOADS_BUTTON_SHOWN("revanced_downloads_enabled", BOOLEAN, TRUE, true), + DOWNLOADS_PACKAGE_NAME("revanced_downloads_package_name", STRING, "org.schabi.newpipe" /* NewPipe */, parents(DOWNLOADS_BUTTON_SHOWN)), // Copy video URL settings - COPY_VIDEO_URL_BUTTON_SHOWN("revanced_copy_video_url_enabled", true, ReturnType.BOOLEAN, true), - COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN("revanced_copy_video_url_timestamp_enabled", true, ReturnType.BOOLEAN, true), + COPY_VIDEO_URL_BUTTON_SHOWN("revanced_copy_video_url_enabled", BOOLEAN, TRUE, true), + COPY_VIDEO_URL_TIMESTAMP_BUTTON_SHOWN("revanced_copy_video_url_timestamp_enabled", BOOLEAN, TRUE, true), // Video settings - OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", true, ReturnType.BOOLEAN), - REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", true, ReturnType.BOOLEAN), - REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", true, ReturnType.BOOLEAN), - REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE("revanced_remember_playback_speed_last_selected_value", 1.0f, ReturnType.FLOAT), + OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", BOOLEAN, TRUE), + REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), + REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE), + REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE("revanced_remember_playback_speed_last_selected_value", FLOAT, 1.0f), // TODO: Unused currently // Whitelist settings - ENABLE_WHITELIST("revanced_whitelist_ads_enabled", false, ReturnType.BOOLEAN), + //ENABLE_WHITELIST("revanced_whitelist_ads_enabled", BOOLEAN, FALSE), // Ad settings - ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", false, ReturnType.BOOLEAN), - ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", "", ReturnType.STRING, true), - VIDEO_ADS_REMOVAL("revanced_video_ads_removal", true, ReturnType.BOOLEAN, true), - ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", true, ReturnType.BOOLEAN), - ADREMOVER_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", true, ReturnType.BOOLEAN), - ADREMOVER_COMMUNITY_POSTS_REMOVAL("revanced_adremover_community_posts_removal", false, ReturnType.BOOLEAN), - ADREMOVER_COMPACT_BANNER_REMOVAL("revanced_adremover_compact_banner_removal", true, ReturnType.BOOLEAN), - ADREMOVER_MOVIE_REMOVAL("revanced_adremover_movie", true, ReturnType.BOOLEAN), - ADREMOVER_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", true, ReturnType.BOOLEAN), - ADREMOVER_SHORTS_REMOVAL("revanced_adremover_shorts", true, ReturnType.BOOLEAN, true), - ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_community_guidelines", true, ReturnType.BOOLEAN), - ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", true, ReturnType.BOOLEAN), - ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL("revanced_adremover_channel_member_shelf_removal", true, ReturnType.BOOLEAN), - ADREMOVER_EMERGENCY_BOX_REMOVAL("revanced_adremover_emergency_box_removal", true, ReturnType.BOOLEAN), - ADREMOVER_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", true, ReturnType.BOOLEAN), - ADREMOVER_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", true, ReturnType.BOOLEAN), - ADREMOVER_PAID_CONTENT_REMOVAL("revanced_adremover_paid_content", true, ReturnType.BOOLEAN), - ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", true, ReturnType.BOOLEAN), - ADREMOVER_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", true, ReturnType.BOOLEAN), - ADREMOVER_SELF_SPONSOR_REMOVAL("revanced_adremover_self_sponsor", true, ReturnType.BOOLEAN), - ADREMOVER_CHAPTER_TEASER_REMOVAL("revanced_adremover_chapter_teaser", true, ReturnType.BOOLEAN), - ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", true, ReturnType.BOOLEAN), - ADREMOVER_GRAY_SEPARATOR("revanced_adremover_separator", true, ReturnType.BOOLEAN), - ADREMOVER_VIEW_PRODUCTS("revanced_adremover_view_products", true, ReturnType.BOOLEAN), - ADREMOVER_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", true, ReturnType.BOOLEAN), - ADREMOVER_CHANNEL_BAR("revanced_hide_channel_bar", false, ReturnType.BOOLEAN), - ADREMOVER_QUICK_ACTIONS("revanced_hide_quick_actions", false, ReturnType.BOOLEAN), - ADREMOVER_RELATED_VIDEOS("revanced_hide_related_videos", false, ReturnType.BOOLEAN), - ADREMOVER_IMAGE_SHELF("revanced_hide_image_shelf", true, ReturnType.BOOLEAN), + ADREMOVER_BUTTONED_REMOVAL("revanced_adremover_buttoned", BOOLEAN, TRUE), + ADREMOVER_CHANNEL_BAR("revanced_hide_channel_bar", BOOLEAN, FALSE), + ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL("revanced_adremover_channel_member_shelf_removal", BOOLEAN, TRUE), + ADREMOVER_CHAPTER_TEASER_REMOVAL("revanced_adremover_chapter_teaser", BOOLEAN, TRUE), + ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_community_guidelines", BOOLEAN, TRUE), + ADREMOVER_COMMUNITY_POSTS_REMOVAL("revanced_adremover_community_posts_removal", BOOLEAN, FALSE), + ADREMOVER_COMPACT_BANNER_REMOVAL("revanced_adremover_compact_banner_removal", BOOLEAN, TRUE), + ADREMOVER_CUSTOM_ENABLED("revanced_adremover_custom_enabled", BOOLEAN, FALSE), + ADREMOVER_CUSTOM_REMOVAL("revanced_adremover_custom_strings", STRING, "", true, parents(ADREMOVER_CUSTOM_ENABLED)), + ADREMOVER_EMERGENCY_BOX_REMOVAL("revanced_adremover_emergency_box_removal", BOOLEAN, TRUE), + ADREMOVER_FEED_SURVEY_REMOVAL("revanced_adremover_feed_survey", BOOLEAN, TRUE), + ADREMOVER_GENERAL_ADS_REMOVAL("revanced_adremover_ad_removal", BOOLEAN, TRUE), + ADREMOVER_GRAY_SEPARATOR("revanced_adremover_separator", BOOLEAN, TRUE), + ADREMOVER_HIDE_CHANNEL_GUIDELINES("revanced_adremover_hide_channel_guidelines", BOOLEAN, TRUE), + ADREMOVER_HIDE_LATEST_POSTS("revanced_adremover_hide_latest_posts", BOOLEAN, TRUE), + ADREMOVER_IMAGE_SHELF("revanced_hide_image_shelf", BOOLEAN, TRUE), + ADREMOVER_INFO_PANEL_REMOVAL("revanced_adremover_info_panel", BOOLEAN, TRUE), + ADREMOVER_MEDICAL_PANEL_REMOVAL("revanced_adremover_medical_panel", BOOLEAN, TRUE), + ADREMOVER_MERCHANDISE_REMOVAL("revanced_adremover_merchandise", BOOLEAN, TRUE), + ADREMOVER_MOVIE_REMOVAL("revanced_adremover_movie", BOOLEAN, TRUE), + ADREMOVER_PAID_CONTENT_REMOVAL("revanced_adremover_paid_content", BOOLEAN, TRUE), + ADREMOVER_QUICK_ACTIONS("revanced_hide_quick_actions", BOOLEAN, FALSE), + ADREMOVER_RELATED_VIDEOS("revanced_hide_related_videos", BOOLEAN, FALSE), + ADREMOVER_SELF_SPONSOR_REMOVAL("revanced_adremover_self_sponsor", BOOLEAN, TRUE), + ADREMOVER_SHORTS_REMOVAL("revanced_adremover_shorts", BOOLEAN, TRUE, true), + ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL("revanced_adremover_subscribers_community_guidelines_removal", BOOLEAN, TRUE), + ADREMOVER_VIEW_PRODUCTS("revanced_adremover_view_products", BOOLEAN, TRUE), + ADREMOVER_WEB_SEARCH_RESULTS("revanced_adremover_web_search_result", BOOLEAN, TRUE), + VIDEO_ADS_REMOVAL("revanced_video_ads_removal", BOOLEAN, TRUE, true), // Action buttons - HIDE_LIKE_BUTTON("revanced_hide_like_button", false, ReturnType.BOOLEAN), - HIDE_DISLIKE_BUTTON("revanced_hide_dislike_button", false, ReturnType.BOOLEAN), - HIDE_DOWNLOAD_BUTTON("revanced_hide_download_button", false, ReturnType.BOOLEAN), - HIDE_PLAYLIST_BUTTON("revanced_hide_playlist_button", false, ReturnType.BOOLEAN), - HIDE_ACTION_BUTTON("revanced_hide_action_button", false, ReturnType.BOOLEAN), - HIDE_SHARE_BUTTON("revanced_hide_share_button", false, ReturnType.BOOLEAN), + HIDE_ACTION_BUTTON("revanced_hide_action_button", BOOLEAN, FALSE), + HIDE_DISLIKE_BUTTON("revanced_hide_dislike_button", BOOLEAN, FALSE), + HIDE_DOWNLOAD_BUTTON("revanced_hide_download_button", BOOLEAN, FALSE), + HIDE_LIKE_BUTTON("revanced_hide_like_button", BOOLEAN, FALSE), + HIDE_PLAYLIST_BUTTON("revanced_hide_playlist_button", BOOLEAN, FALSE), + HIDE_SHARE_BUTTON("revanced_hide_share_button", BOOLEAN, FALSE), // Layout settings - DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", false, ReturnType.BOOLEAN), - PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", false, ReturnType.BOOLEAN), - USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", false, ReturnType.BOOLEAN, true), - SPOOF_APP_VERSION("revanced_spoof_app_version", false, ReturnType.BOOLEAN, true), - WIDE_SEARCHBAR("revanced_wide_searchbar", false, ReturnType.BOOLEAN, true), - HIDE_ALBUM_CARDS("revanced_hide_album_cards", false, ReturnType.BOOLEAN, true), - HIDE_ARTIST_CARDS("revanced_hide_artist_cards", false, ReturnType.BOOLEAN), - HIDE_AUTOPLAY_BUTTON("revanced_hide_autoplay_button", true, ReturnType.BOOLEAN, true), - HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", true, ReturnType.BOOLEAN), - HIDE_CAPTIONS_BUTTON("revanced_hide_captions_button", false, ReturnType.BOOLEAN), - HIDE_CAST_BUTTON("revanced_hide_cast_button", true, ReturnType.BOOLEAN, true), - HIDE_COMMENTS_SECTION("revanced_hide_comments_section", false, ReturnType.BOOLEAN, true), - HIDE_CREATE_BUTTON("revanced_hide_create_button", true, ReturnType.BOOLEAN, true), - HIDE_CROWDFUNDING_BOX("revanced_hide_crowdfunding_box", false, ReturnType.BOOLEAN, true), - HIDE_EMAIL_ADDRESS("revanced_hide_email_address", false, ReturnType.BOOLEAN), - HIDE_ENDSCREEN_CARDS("revanced_hide_endscreen_cards", true, ReturnType.BOOLEAN), - HIDE_FULLSCREEN_PANELS("revanced_hide_fullscreen_panels", true, ReturnType.BOOLEAN), //ToDo: Add to prefs - HIDE_INFO_CARDS("revanced_hide_infocards", true, ReturnType.BOOLEAN), - HIDE_MIX_PLAYLISTS("revanced_hide_mix_playlists", false, ReturnType.BOOLEAN, true), - HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", false, ReturnType.BOOLEAN, true), - HIDE_REEL_BUTTON("revanced_hide_reel_button", true, ReturnType.BOOLEAN, true), - HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", true, ReturnType.BOOLEAN, true), - HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", false, ReturnType.BOOLEAN), - HIDE_TIMESTAMP("revanced_hide_timestamp", false, ReturnType.BOOLEAN), - HIDE_SEEKBAR("revanced_hide_seekbar", false, ReturnType.BOOLEAN), - HIDE_WATCH_IN_VR("revanced_hide_watch_in_vr", false, ReturnType.BOOLEAN, true), - HIDE_BREAKING_NEWS("revanced_hide_breaking_news", true, ReturnType.BOOLEAN, true), - HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", false, ReturnType.BOOLEAN), - HIDE_FLOATING_MICROPHONE_BUTTON("revanced_hide_floating_microphone_button", true, ReturnType.BOOLEAN, true), + DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", BOOLEAN, FALSE), + HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true), + HIDE_ARTIST_CARDS("revanced_hide_artist_cards", BOOLEAN, FALSE), + HIDE_AUTOPLAY_BUTTON("revanced_hide_autoplay_button", BOOLEAN, TRUE, true), + HIDE_BREAKING_NEWS("revanced_hide_breaking_news", BOOLEAN, TRUE, true), + HIDE_CAPTIONS_BUTTON("revanced_hide_captions_button", BOOLEAN, FALSE), + HIDE_CAST_BUTTON("revanced_hide_cast_button", BOOLEAN, TRUE, true), + HIDE_COMMENTS_SECTION("revanced_hide_comments_section", BOOLEAN, FALSE, true), + HIDE_CREATE_BUTTON("revanced_hide_create_button", BOOLEAN, TRUE, true), + HIDE_CROWDFUNDING_BOX("revanced_hide_crowdfunding_box", BOOLEAN, FALSE, true), + HIDE_EMAIL_ADDRESS("revanced_hide_email_address", BOOLEAN, FALSE), + HIDE_ENDSCREEN_CARDS("revanced_hide_endscreen_cards", BOOLEAN, TRUE), + HIDE_FLOATING_MICROPHONE_BUTTON("revanced_hide_floating_microphone_button", BOOLEAN, TRUE, true), + HIDE_FULLSCREEN_PANELS("revanced_hide_fullscreen_panels", BOOLEAN, TRUE), + HIDE_INFO_CARDS("revanced_hide_infocards", BOOLEAN, TRUE), + HIDE_MIX_PLAYLISTS("revanced_hide_mix_playlists", BOOLEAN, FALSE, true), + HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", BOOLEAN, FALSE), + HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", BOOLEAN, FALSE, true), + HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE), + HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", BOOLEAN, TRUE, true), + HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE), + HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE), + HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE), + HIDE_WATCH_IN_VR("revanced_hide_watch_in_vr", BOOLEAN, FALSE, true), + PLAYER_POPUP_PANELS("revanced_player_popup_panels_enabled", BOOLEAN, FALSE), + SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"), + USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true), + WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true), // Misc. Settings - SIGNATURE_SPOOFING("revanced_spoof_signature_verification", true, ReturnType.BOOLEAN), - CAPTIONS_ENABLED("revanced_autocaptions_enabled", false, ReturnType.BOOLEAN), - PREFERRED_AUTO_REPEAT("revanced_pref_auto_repeat", false, ReturnType.BOOLEAN), - USE_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", true, ReturnType.BOOLEAN), - TAP_SEEKING_ENABLED("revanced_enable_tap_seeking", true, ReturnType.BOOLEAN), - ENABLE_MINIMIZED_PLAYBACK("revanced_enable_minimized_playback", true, ReturnType.BOOLEAN), - OPEN_LINKS_DIRECTLY("revanced_uri_redirect", true, ReturnType.BOOLEAN, true), - DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", true, ReturnType.BOOLEAN), - ENABLE_EXTERNAL_BROWSER("revanced_enable_external_browser", true, ReturnType.BOOLEAN, true), + SIGNATURE_SPOOFING("revanced_spoof_signature_verification", BOOLEAN, TRUE, "revanced_spoof_signature_verification_user_dialog_message"), + CAPTIONS_ENABLED("revanced_autocaptions_enabled", BOOLEAN, FALSE), + DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", BOOLEAN, TRUE), + ENABLE_EXTERNAL_BROWSER("revanced_enable_external_browser", BOOLEAN, TRUE, true), + ENABLE_MINIMIZED_PLAYBACK("revanced_enable_minimized_playback", BOOLEAN, TRUE), + PREFERRED_AUTO_REPEAT("revanced_pref_auto_repeat", BOOLEAN, FALSE), + TAP_SEEKING_ENABLED("revanced_enable_tap_seeking", BOOLEAN, TRUE), + USE_HDR_AUTO_BRIGHTNESS("revanced_pref_hdr_autobrightness", BOOLEAN, TRUE), // Swipe controls - ENABLE_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", true, ReturnType.BOOLEAN), - ENABLE_SWIPE_VOLUME("revanced_enable_swipe_volume", true, ReturnType.BOOLEAN), - ENABLE_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", false, ReturnType.BOOLEAN), - ENABLE_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", true, ReturnType.BOOLEAN), - SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", 500L, ReturnType.LONG), - SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_overlay_text_size", 22f, ReturnType.FLOAT), - SWIPE_OVERLAY_BACKGROUND_ALPHA("revanced_swipe_overlay_background_alpha", 127, ReturnType.INTEGER), - SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_magnitude_threshold", 30f, ReturnType.FLOAT), + ENABLE_SWIPE_BRIGHTNESS("revanced_enable_swipe_brightness", BOOLEAN, TRUE), + ENABLE_SWIPE_VOLUME("revanced_enable_swipe_volume", BOOLEAN, TRUE), + ENABLE_PRESS_TO_SWIPE("revanced_enable_press_to_swipe", BOOLEAN, FALSE, + parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + ENABLE_SWIPE_HAPTIC_FEEDBACK("revanced_enable_swipe_haptic_feedback", BOOLEAN, TRUE, + parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_magnitude_threshold", FLOAT, 30f, + parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + SWIPE_OVERLAY_BACKGROUND_ALPHA("revanced_swipe_overlay_background_alpha", INTEGER, 127, + parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_overlay_text_size", FLOAT, 22f, + parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), + SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", LONG, 500L, + parents(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)), // Debug settings - DEBUG("revanced_debug_enabled", false, ReturnType.BOOLEAN), - DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", false, ReturnType.BOOLEAN), - DEBUG_SHOW_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", true, ReturnType.BOOLEAN), + DEBUG("revanced_debug_enabled", BOOLEAN, FALSE), + DEBUG_STACKTRACE("revanced_debug_stacktrace_enabled", BOOLEAN, FALSE, parents(DEBUG)), + DEBUG_SHOW_TOAST_ON_ERROR("revanced_debug_toast_on_error_enabled", BOOLEAN, TRUE, "revanced_debug_toast_on_error_user_dialog_message"), - USE_DARK_THEME("app_theme_dark", false, ReturnType.BOOLEAN), - - // RYD settings - RYD_USER_ID("ryd_userId", null, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.STRING), - RYD_ENABLED("ryd_enabled", true, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN), - RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", false, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN), - RYD_USE_COMPACT_LAYOUT("ryd_use_compact_layout", false, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN), + // ReturnYoutubeDislike settings + RYD_ENABLED("ryd_enabled", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE), + RYD_USER_ID("ryd_userId", STRING, "", RETURN_YOUTUBE_DISLIKE), + RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), + RYD_USE_COMPACT_LAYOUT("ryd_use_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)), // SponsorBlock settings - SB_ENABLED("sb-enabled", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_VOTING_ENABLED("sb-voting-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_CREATE_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_USE_COMPACT_SKIPBUTTON("sb-use-compact-skip-button", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_SHOW_TOAST_ON_SKIP("show-toast", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_TRACK_SKIP_COUNT("count-skips", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_UUID("uuid", "", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING), - SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", 150, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER), - SB_MIN_DURATION("sb-min-duration", 0F, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.FLOAT), - SB_SEEN_GUIDELINES("sb-seen-gl", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED("sb-skipped-segments", 0, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER), - SB_SKIPPED_SEGMENTS_TIME_SAVED("sb-skipped-segments-time", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG), - SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_IS_VIP("sb-is-vip", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN), - SB_LAST_VIP_CHECK("sb-last-vip-check", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG), - SB_API_URL("sb-api-host-url", "https://sponsor.ajay.app", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING); + SB_ENABLED("sb-enabled", BOOLEAN, TRUE, SPONSOR_BLOCK), + SB_VOTING_ENABLED("sb-voting-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_CREATE_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_USE_COMPACT_SKIPBUTTON("sb-use-compact-skip-button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_SHOW_TOAST_ON_SKIP("show-toast", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_TRACK_SKIP_COUNT("count-skips", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_UUID("uuid", STRING, "", SPONSOR_BLOCK), + SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", INTEGER, 150, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_MIN_DURATION("sb-min-duration", FLOAT, 0F, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_SEEN_GUIDELINES("sb-seen-gl", BOOLEAN, FALSE, SPONSOR_BLOCK), + SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED("sb-skipped-segments", INTEGER, 0, SPONSOR_BLOCK), + SB_SKIPPED_SEGMENTS_TIME_SAVED("sb-skipped-segments-time", LONG, 0L, SPONSOR_BLOCK), + SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)), + SB_IS_VIP("sb-is-vip", BOOLEAN, FALSE, SPONSOR_BLOCK), + SB_LAST_VIP_CHECK("sb-last-vip-check", LONG, 0L, SPONSOR_BLOCK), + SB_API_URL("sb-api-host-url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK); - private final String path; - private final Object defaultValue; - private final SharedPrefHelper.SharedPrefNames sharedPref; - private final ReturnType returnType; - private final boolean rebootApp; + private static SettingsEnum[] parents(SettingsEnum ... parents) { + return parents; + } - // must be volatile, as some settings are read/write from different threads - // of note, the object value is persistently stored using SharedPreferences (which is thread safe) + @NonNull + public final String path; + @NonNull + public final Object defaultValue; + @NonNull + public final SharedPrefCategory sharedPref; + @NonNull + public final ReturnType returnType; + /** + * If the app should be rebooted, if this setting is changed + */ + public final boolean rebootApp; + /** + * Set of boolean parent settings. + * If any of the parents are enabled, then this setting is available to configure. + * + * For example: {@link #DEBUG_STACKTRACE} is non-functional and cannot be configured, + * unless it's parent {@link #DEBUG} is enabled. + * + * Declaration is not needed for items that do not appear in the ReVanced Settings UI. + */ + @Nullable + private final SettingsEnum[] parents; + + /** + * Confirmation message to display, if the user tries to change the setting from the default value. + * Can only be used for {@link ReturnType#BOOLEAN} setting types. + */ + @Nullable + public final StringRef userDialogMessage; + + // Must be volatile, as some settings are read/write from different threads. + // Of note, the object value is persistently stored using SharedPreferences (which is thread safe). + @NonNull private volatile Object value; - SettingsEnum(String path, Object defaultValue, ReturnType returnType) { - this(path, defaultValue, SharedPrefHelper.SharedPrefNames.YOUTUBE, returnType, false); + SettingsEnum(String path, ReturnType returnType, Object defaultValue) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, null); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, + boolean rebootApp) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null,null); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, + String userDialogMessage) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, userDialogMessage, null); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, + SettingsEnum[] parents) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, parents); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, + boolean rebootApp, String userDialogMessage) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, null); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, + boolean rebootApp, SettingsEnum[] parents) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, parents); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, + boolean rebootApp, String userDialogMessage, SettingsEnum[] parents) { + this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, parents); } - SettingsEnum(String path, Object defaultValue, ReturnType returnType, boolean rebootApp) { - this(path, defaultValue, SharedPrefHelper.SharedPrefNames.YOUTUBE, returnType, rebootApp); + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName) { + this(path, returnType, defaultValue, prefName, false, null, null); } - - SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType) { - this(path, defaultValue, prefName, returnType, false); + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, + boolean rebootApp) { + this(path, returnType, defaultValue, prefName, rebootApp, null, null); } - - SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType, boolean rebootApp) { - this.path = path; - this.defaultValue = defaultValue; - this.sharedPref = prefName; - this.returnType = returnType; + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, + String userDialogMessage) { + this(path, returnType, defaultValue, prefName, false, userDialogMessage, null); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, + SettingsEnum[] parents) { + this(path, returnType, defaultValue, prefName, false, null, parents); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName, + boolean rebootApp, @Nullable String userDialogMessage, @Nullable SettingsEnum[] parents) { + this.path = Objects.requireNonNull(path); + this.returnType = Objects.requireNonNull(returnType); + this.value = this.defaultValue = Objects.requireNonNull(defaultValue); + this.sharedPref = Objects.requireNonNull(prefName); this.rebootApp = rebootApp; + + if (userDialogMessage == null) { + this.userDialogMessage = null; + } else { + if (returnType != ReturnType.BOOLEAN) { + throw new IllegalArgumentException("must be Boolean type: " + path); + } + this.userDialogMessage = new StringRef(userDialogMessage); + } + + this.parents = parents; + if (parents != null) { + for (SettingsEnum parent : parents) { + if (parent.returnType != ReturnType.BOOLEAN) { + throw new IllegalArgumentException("parent must be Boolean type: " + parent); + } + } + } } static { loadAllSettings(); } - private static void loadAllSettings() { - if (ReVancedUtils.getContext() == null) { - LogHelper.printException(() -> "SettingsEnum loaded before ReVancedUtils context was set"); - return; + @Nullable + public static SettingsEnum settingFromPath(@NonNull String str) { + for (SettingsEnum setting : values()) { + if (setting.path.equals(str)) return setting; } + return null; + } + + private static void loadAllSettings() { for (SettingsEnum setting : values()) { setting.load(); } @@ -192,64 +291,78 @@ public enum SettingsEnum { private void load() { switch (returnType) { - case FLOAT: - value = SharedPrefHelper.getFloat(sharedPref, path, (float) defaultValue); - break; - case LONG: - value = SharedPrefHelper.getLong(sharedPref, path, (long) defaultValue); - break; case BOOLEAN: - value = SharedPrefHelper.getBoolean(sharedPref, path, (boolean) defaultValue); + value = sharedPref.getBoolean(path, (boolean) defaultValue); break; case INTEGER: - value = SharedPrefHelper.getInt(sharedPref, path, (int) defaultValue); + value = sharedPref.getInt(path, (Integer) defaultValue); + break; + case LONG: + value = sharedPref.getLong(path, (Long) defaultValue); + break; + case FLOAT: + value = sharedPref.getFloat(path, (Float) defaultValue); break; case STRING: - value = SharedPrefHelper.getString(sharedPref, path, (String) defaultValue); + value = sharedPref.getString(path, (String) defaultValue); break; default: - LogHelper.printException(() -> "Setting does not have a valid Type: " + name()); - break; + throw new IllegalStateException(name()); } } /** * Sets, but does _not_ persistently save the value. * - * @see #saveValue(Object) + * This intentionally is a static method, to deter accidental usage + * when {@link #saveValue(Object)} was intended. */ - public void setValue(Object newValue) { - this.value = newValue; + public static void setValue(@NonNull SettingsEnum setting, @NonNull Object newValue) { + setting.value = Objects.requireNonNull(newValue); } /** - * Sets the value, and persistently saves it + * Sets the value, and persistently saves it. */ - public void saveValue(Object newValue) { + public void saveValue(@NonNull Object newValue) { + Objects.requireNonNull(newValue); switch (returnType) { - case FLOAT: - SharedPrefHelper.saveFloat(sharedPref, path, (float) newValue); - break; - case LONG: - SharedPrefHelper.saveLong(sharedPref, path, (long) newValue); - break; case BOOLEAN: - SharedPrefHelper.saveBoolean(sharedPref, path, (boolean) newValue); + sharedPref.saveBoolean(path, (boolean) newValue); break; case INTEGER: - SharedPrefHelper.saveInt(sharedPref, path, (int) newValue); + sharedPref.saveInt(path, (int) newValue); + break; + case LONG: + sharedPref.saveLong(path, (long) newValue); + break; + case FLOAT: + sharedPref.saveFloat(path, (float) newValue); break; case STRING: - SharedPrefHelper.saveString(sharedPref, path, (String) newValue); + sharedPref.saveString(path, (String) newValue); break; default: - LogHelper.printException(() -> "Setting does not have a valid Type: " + name()); - break; + throw new IllegalStateException(name()); } - value = newValue; } + /** + * @return if this setting can be configured and used. + * + * Not to be confused with {@link #getBoolean()} + */ + public boolean isAvailable() { + if (parents == null) { + return true; + } + for (SettingsEnum parent : parents) { + if (parent.getBoolean()) return true; + } + return false; + } + public boolean getBoolean() { return (Boolean) value; } @@ -266,23 +379,16 @@ public enum SettingsEnum { return (Float) value; } + @NonNull public String getString() { return (String) value; } - public Object getDefaultValue() { - return defaultValue; + public enum ReturnType { + BOOLEAN, + INTEGER, + STRING, + LONG, + FLOAT, } - - public String getPath() { - return path; - } - - public ReturnType getReturnType() { - return returnType; - } - - public boolean shouldRebootOnChange() { - return rebootApp; - } -} +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java new file mode 100644 index 00000000..59413da3 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java @@ -0,0 +1,97 @@ +package app.revanced.integrations.settings; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +import app.revanced.integrations.utils.ReVancedUtils; + +public enum SharedPrefCategory { + YOUTUBE("youtube"), + RETURN_YOUTUBE_DISLIKE("ryd"), + SPONSOR_BLOCK("sponsor-block"), + REVANCED_PREFS("revanced_prefs"); + + @NonNull + public final String prefName; + @NonNull + public final SharedPreferences preferences; + + SharedPrefCategory(@NonNull String prefName) { + this.prefName = Objects.requireNonNull(prefName); + preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE); + } + + public void saveString(@NonNull String key, @NonNull String value) { + Objects.requireNonNull(value); + preferences.edit().putString(key, value).apply(); + } + + public void saveBoolean(@NonNull String key, boolean value) { + preferences.edit().putBoolean(key, value).apply(); + } + + public void saveInt(@NonNull String key, int value) { + preferences.edit().putInt(key, value).apply(); + } + + public void saveLong(@NonNull String key, long value) { + preferences.edit().putLong(key, value).apply(); + } + + public void saveFloat(@NonNull String key, float value) { + preferences.edit().putFloat(key, value).apply(); + } + + + @NonNull + public String getString(@NonNull String key, @NonNull String _default) { + Objects.requireNonNull(_default); + return preferences.getString(key, _default); + } + + public boolean getBoolean(@NonNull String key, boolean _default) { + return preferences.getBoolean(key, _default); + } + + // region Hack, required for PreferencesFragments to function correctly. unknown why required + + @NonNull + public Integer getInt(@NonNull String key, @NonNull Integer _default) { + try { + return Integer.valueOf(preferences.getString(key, _default.toString())); + } catch (ClassCastException ex) { + return preferences.getInt(key, _default); + } + } + + @NonNull + public Long getLong(@NonNull String key, @NonNull Long _default) { + try { + return Long.valueOf(preferences.getString(key, _default.toString())); + } catch (ClassCastException ex) { + return preferences.getLong(key, _default); + } + } + + @NonNull + public Float getFloat(@NonNull String key, @NonNull Float _default) { + try { + return Float.valueOf(preferences.getString(key, _default.toString())); + } catch (ClassCastException ex) { + return preferences.getFloat(key, _default); + } + } + + // endregion + + + @NonNull + @Override + public String toString() { + return prefName; + } +} diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java index 898d21c9..cda92cb9 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingActivity.java @@ -16,6 +16,9 @@ import app.revanced.integrations.utils.ThemeHelper; public class ReVancedSettingActivity { + /** + * Injection point. + */ public static void setTheme(LicenseActivity base) { final var whiteTheme = "Theme.YouTube.Settings"; final var darkTheme = "Theme.YouTube.Settings.Dark"; @@ -26,6 +29,9 @@ public class ReVancedSettingActivity { base.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style")); } + /** + * Injection point. + */ public static void initializeSettings(LicenseActivity base) { base.setContentView(ReVancedUtils.getResourceIdentifier("revanced_settings_with_toolbar", "layout")); diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index 308f348d..43a240dd 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -15,152 +15,148 @@ import android.os.Process; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; +import android.preference.PreferenceManager; import android.preference.SwitchPreference; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.google.android.apps.youtube.app.application.Shell_HomeActivity; -import java.util.List; - import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; public class ReVancedSettingsFragment extends PreferenceFragment { - - private List screens; - - private boolean Registered = false; - private boolean settingsInitialized = false; - - private final CharSequence[] videoSpeedEntries = {"Auto", "0.25x", "0.5x", "0.75x", "Normal", "1.25x", "1.5x", "1.75x", "2x", "3x", "4x", "5x"}; - private final CharSequence[] videoSpeedentryValues = {"-2", "0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "3.0", "4.0", "5.0"}; - //private final CharSequence[] buttonLocationEntries = {"None", "In player", "Under player", "Both"}; - //private final CharSequence[] buttonLocationentryValues = {"NONE", "PLAYER", "BUTTON_BAR", "BOTH"}; + /** + * If a dialog is currently being shown. Used to prevent showing additional dialogs if user cancels a dialog. + */ + private static boolean currentlyShowingDialog; SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { - for (SettingsEnum setting : SettingsEnum.values()) { - if (!setting.getPath().equals(str)) continue; - Preference pref = this.findPreferenceOnScreen(str); - - LogHelper.printDebug(() -> "Setting " + setting.name() + " was changed. Preference " + str + ": " + pref.toString()); + try { + SettingsEnum setting = SettingsEnum.settingFromPath(str); + if (setting == null) { + return; + } + Preference pref = this.findPreference(str); + LogHelper.printDebug(() -> "Setting " + setting.name() + " was changed. Preference " + str + ": " + pref); if (pref instanceof SwitchPreference) { SwitchPreference switchPref = (SwitchPreference) pref; - setting.setValue(switchPref.isChecked()); + SettingsEnum.setValue(setting, switchPref.isChecked()); } else if (pref instanceof EditTextPreference) { - EditTextPreference editPref = (EditTextPreference) pref; - Object value = null; - switch (setting.getReturnType()) { - case FLOAT: - value = Float.parseFloat(editPref.getText()); + String editText = ((EditTextPreference) pref).getText(); + Object value; + switch (setting.returnType) { + case INTEGER: + value = Integer.parseInt(editText); break; case LONG: - value = Long.parseLong(editPref.getText()); + value = Long.parseLong(editText); + break; + case FLOAT: + value = Float.parseFloat(editText); break; case STRING: - value = editPref.getText(); - break; - case INTEGER: - value = Integer.parseInt(editPref.getText()); + value = editText; break; default: - LogHelper.printException(() -> "Setting has no valid return type! " + setting.getReturnType()); - break; + throw new IllegalStateException(setting.toString()); } - setting.setValue(value); + SettingsEnum.setValue(setting, value); } else { - LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref.toString()); + LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref); } - if (ReVancedUtils.getContext() != null && settingsInitialized && setting.shouldRebootOnChange()) { - rebootDialog(getActivity()); + if (!currentlyShowingDialog) { + if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { + showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting); + } else if (setting.rebootApp) { + rebootDialog(getActivity()); + } } + + enableDisablePreferences(); + } catch (Exception ex) { + LogHelper.printException(() -> "OnSharedPreferenceChangeListener failure", ex); } }; @SuppressLint("ResourceType") - @Override // android.preference.PreferenceFragment, android.app.Fragment - public void onCreate(Bundle bundle) { - super.onCreate(bundle); - getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.YOUTUBE.getName()); + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); try { + PreferenceManager preferenceManager = getPreferenceManager(); + preferenceManager.setSharedPreferencesName(SharedPrefCategory.YOUTUBE.prefName); addPreferencesFromResource(ReVancedUtils.getResourceIdentifier("revanced_prefs", "xml")); - SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); - this.settingsInitialized = sharedPreferences.getBoolean("revanced_initialized", false); - sharedPreferences.registerOnSharedPreferenceChangeListener(this.listener); - this.Registered = true; + enableDisablePreferences(); - this.settingsInitialized = true; - } catch (Throwable th) { - LogHelper.printException(() -> "Error during onCreate()", th); + preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); + } catch (Exception ex) { + LogHelper.printException(() -> "onActivityCreated() error", ex); } } @Override // android.preference.PreferenceFragment, android.app.Fragment public void onDestroy() { - if (this.Registered) { - getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this.listener); - this.Registered = false; - } + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); super.onDestroy(); } - private Preference findPreferenceOnScreen(CharSequence key) { - if (key == null) { - LogHelper.printException(() -> "Key cannot be null!"); - return null; - } - Preference pref = null; - if (this.findPreference(key) != null) { - pref = this.findPreference(key); - } else { - for (PreferenceScreen screen : this.screens) { - Preference toCheck = screen.findPreference(key); - if (toCheck == null) continue; - pref = toCheck; - LogHelper.printDebug(() -> "Found preference " + key + " on screen: " + screen.getTitle()); + private void enableDisablePreferences() { + for (SettingsEnum setting : SettingsEnum.values()) { + Preference preference = this.findPreference(setting.path); + if (preference != null) { + preference.setEnabled(setting.isAvailable()); } } - - return pref; } - /* - private void setCopyLinkListPreferenceData(ListPreference listPreference, String str) { - listPreference.setEntries(this.buttonLocationEntries); - listPreference.setEntryValues(this.buttonLocationentryValues); - String string = this.sharedPreferences.getString(str, "NONE"); - if (listPreference.getValue() == null) { - listPreference.setValue(string); - } - listPreference.setSummary(this.buttonLocationEntries[listPreference.findIndexOfValue(string)]); - } - */ - - private String getPackageName() { - Context context = ReVancedUtils.getContext(); - if (context == null) { - LogHelper.printException(() -> "Context is null, returning com.google.android.youtube!"); - return "com.google.android.youtube"; - } - String PACKAGE_NAME = context.getPackageName(); - LogHelper.printDebug(() -> "getPackageName: " + PACKAGE_NAME); - - return PACKAGE_NAME; - } - - private void reboot(Activity activity, Class homeActivityClass) { - int intent; - intent = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - ((AlarmManager) activity.getSystemService(Context.ALARM_SERVICE)).setExact(AlarmManager.ELAPSED_REALTIME, 1500L, PendingIntent.getActivity(activity, 0, new Intent(activity, Shell_HomeActivity.class), intent)); + private void reboot(@NonNull Activity activity) { + final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + PendingIntent intent = PendingIntent.getActivity(activity, 0, + new Intent(activity, Shell_HomeActivity.class), intentFlags); + AlarmManager systemService = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); + systemService.setExact(AlarmManager.ELAPSED_REALTIME, 1500L, intent); Process.killProcess(Process.myPid()); } - private void rebootDialog(final Activity activity) { - new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")).setPositiveButton(str("in_app_update_restart_button"), (dialog, id) -> reboot(activity, Shell_HomeActivity.class)).setNegativeButton(str("sign_in_cancel"), null).show(); + private void rebootDialog(@NonNull Activity activity) { + currentlyShowingDialog = true; + String positiveButton = str("in_app_update_restart_button"); + String negativeButton = str("sign_in_cancel"); + new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")) + .setPositiveButton(positiveButton, (dialog, id) -> { + reboot(activity); + currentlyShowingDialog = false; + }) + .setNegativeButton(negativeButton, (dialog, id) -> { + currentlyShowingDialog = false; + }).show(); + } + + private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) { + currentlyShowingDialog = true; + new AlertDialog.Builder(activity) + .setTitle(str("revanced_settings_confirm_user_dialog_title")) + .setMessage(setting.userDialogMessage.toString()) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + if (setting.rebootApp) { + rebootDialog(activity); + } else { + currentlyShowingDialog = false; + } + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + Boolean defaultBooleanValue = (Boolean) setting.defaultValue; + SettingsEnum.setValue(setting, defaultBooleanValue); + switchPref.setChecked(defaultBooleanValue); + currentlyShowingDialog = false; + }).show(); } } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index 41d5eb21..6664861f 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -15,7 +15,7 @@ import android.preference.SwitchPreference; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.SharedPrefHelper; +import app.revanced.integrations.settings.SharedPrefCategory; public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { @@ -35,27 +35,25 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { private SwitchPreference compactLayoutPreference; private void updateUIState() { - final boolean rydIsEnabled = SettingsEnum.RYD_ENABLED.getBoolean(); - - enabledPreference.setSummary(rydIsEnabled + enabledPreference.setSummary(SettingsEnum.RYD_ENABLED.getBoolean() ? str("revanced_ryd_enable_summary_on") : str("revanced_ryd_enable_summary_off")); percentagePreference.setSummary(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean() ? str("revanced_ryd_dislike_percentage_summary_on") : str("revanced_ryd_dislike_percentage_summary_off")); - percentagePreference.setEnabled(rydIsEnabled); + percentagePreference.setEnabled(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.isAvailable()); compactLayoutPreference.setSummary(SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean() ? str("revanced_ryd_compact_layout_summary_on") : str("revanced_ryd_compact_layout_summary_off")); - compactLayoutPreference.setEnabled(rydIsEnabled); + compactLayoutPreference.setEnabled(SettingsEnum.RYD_USE_COMPACT_LAYOUT.isAvailable()); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.RYD.getName()); + getPreferenceManager().setSharedPreferencesName(SharedPrefCategory.RETURN_YOUTUBE_DISLIKE.prefName); Activity context = this.getActivity(); PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); @@ -78,7 +76,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { percentagePreference.setChecked(SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()); percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title")); percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> { - SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue((Boolean)newValue); + SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue(newValue); updateUIState(); return true; @@ -89,7 +87,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { compactLayoutPreference.setChecked(SettingsEnum.RYD_USE_COMPACT_LAYOUT.getBoolean()); compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title")); compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> { - SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue((Boolean)newValue); + SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue(newValue); updateUIState(); return true; diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java index f1c1bbff..05da518b 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; import java.text.DecimalFormat; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.sponsorblock.SegmentPlaybackController; import app.revanced.integrations.sponsorblock.SponsorBlockSettings; import app.revanced.integrations.sponsorblock.SponsorBlockUtils; @@ -38,7 +39,6 @@ import app.revanced.integrations.sponsorblock.requests.SBRequester; import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; @SuppressWarnings("deprecation") public class SponsorBlockSettingsFragment extends PreferenceFragment { @@ -115,7 +115,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { super.onCreate(savedInstanceState); try { PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK.getName()); + preferenceManager.setSharedPreferencesName(SharedPrefCategory.SPONSOR_BLOCK.prefName); Activity context = this.getActivity(); PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(context); @@ -304,7 +304,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { - SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.getDefaultValue()); + SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.defaultValue); ReVancedUtils.showToastLong(str("sb_api_url_reset")); } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { String serverAddress = editText.getText().toString(); @@ -522,8 +522,8 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { new AlertDialog.Builder(preference1.getContext()) .setTitle(str("sb_stats_self_saved_reset_title")) .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { - SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getDefaultValue()); - SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getDefaultValue()); + SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.defaultValue); + SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.defaultValue); updateStatsSelfSaved.run(); }) .setNegativeButton(android.R.string.no, null).show(); diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java index 22923d55..f8f4fffa 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java @@ -14,11 +14,11 @@ import org.json.JSONObject; import java.util.UUID; import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour; import app.revanced.integrations.sponsorblock.objects.SegmentCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.SharedPrefHelper; public class SponsorBlockSettings { @@ -55,7 +55,7 @@ public class SponsorBlockSettings { } SegmentCategory.updateEnabledCategories(); - SharedPreferences.Editor editor = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK).edit(); + SharedPreferences.Editor editor = SharedPrefCategory.SPONSOR_BLOCK.preferences.edit(); for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { category.save(editor); } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java index f23160a0..a11703da 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java @@ -21,8 +21,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.SharedPrefHelper; import app.revanced.integrations.utils.StringRef; public enum SegmentCategory { @@ -84,7 +84,7 @@ public enum SegmentCategory { } public static void loadFromPreferences() { - SharedPreferences preferences = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK); + SharedPreferences preferences = SharedPrefCategory.SPONSOR_BLOCK.preferences; LogHelper.printDebug(() -> "loadFromPreferences"); for (SegmentCategory category : valuesWithoutUnsubmitted()) { category.load(preferences); diff --git a/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java b/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java deleted file mode 100644 index d792bc07..00000000 --- a/app/src/main/java/app/revanced/integrations/utils/SharedPrefHelper.java +++ /dev/null @@ -1,94 +0,0 @@ -package app.revanced.integrations.utils; - -import android.content.Context; -import android.content.SharedPreferences; - -import java.util.Objects; - -public class SharedPrefHelper { - public static void saveString(SharedPrefNames prefName, String key, String value) { - getPreferences(prefName).edit().putString(key, value).apply(); - } - - public static void saveBoolean(SharedPrefNames prefName, String key, boolean value) { - getPreferences(prefName).edit().putBoolean(key, value).apply(); - } - - public static void saveFloat(SharedPrefNames prefName, String key, float value) { - getPreferences(prefName).edit().putFloat(key, value).apply(); - } - - public static void saveInt(SharedPrefNames prefName, String key, int value) { - getPreferences(prefName).edit().putInt(key, value).apply(); - } - - public static void saveLong(SharedPrefNames prefName, String key, long value) { - getPreferences(prefName).edit().putLong(key, value).apply(); - } - - public static String getString(SharedPrefNames prefName, String key, String _default) { - return getPreferences(prefName).getString(key, _default); - } - - public static boolean getBoolean(SharedPrefNames prefName, String key, boolean _default) { - return getPreferences(prefName).getBoolean(key, _default); - } - - // region Hack, unknown why required - - public static Long getLong(SharedPrefNames prefName, String key, long _default) { - SharedPreferences sharedPreferences = getPreferences(prefName); - try { - return Long.valueOf(sharedPreferences.getString(key, _default + "")); - } catch (ClassCastException ex) { - return sharedPreferences.getLong(key, _default); - } - } - - public static Float getFloat(SharedPrefNames prefName, String key, float _default) { - SharedPreferences sharedPreferences = getPreferences(prefName); - try { - return Float.valueOf(sharedPreferences.getString(key, _default + "")); - } catch (ClassCastException ex) { - return sharedPreferences.getFloat(key, _default); - } - } - - public static Integer getInt(SharedPrefNames prefName, String key, int _default) { - SharedPreferences sharedPreferences = getPreferences(prefName); - try { - return Integer.valueOf(sharedPreferences.getString(key, _default + "")); - } catch (ClassCastException ex) { - return sharedPreferences.getInt(key, _default); - } - } - - // endregion - - public static SharedPreferences getPreferences(SharedPrefNames name) { - return Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(name.getName(), Context.MODE_PRIVATE); - } - - public enum SharedPrefNames { - - YOUTUBE("youtube"), - RYD("ryd"), - SPONSOR_BLOCK("sponsor-block"), - REVANCED_PREFS("revanced_prefs"); - - private final String name; - - SharedPrefNames(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } - } -} diff --git a/app/src/main/java/app/revanced/tiktok/settings/ReturnType.java b/app/src/main/java/app/revanced/tiktok/settings/ReturnType.java deleted file mode 100644 index bd64c8ea..00000000 --- a/app/src/main/java/app/revanced/tiktok/settings/ReturnType.java +++ /dev/null @@ -1,5 +0,0 @@ -package app.revanced.tiktok.settings; - -public enum ReturnType { - BOOLEAN, INTEGER, STRING, LONG, FLOAT -} diff --git a/app/src/main/java/app/revanced/tiktok/settings/SettingsEnum.java b/app/src/main/java/app/revanced/tiktok/settings/SettingsEnum.java index a114b429..2318c869 100644 --- a/app/src/main/java/app/revanced/tiktok/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/tiktok/settings/SettingsEnum.java @@ -1,104 +1,112 @@ package app.revanced.tiktok.settings; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.tiktok.settings.SettingsEnum.ReturnType.BOOLEAN; +import static app.revanced.tiktok.settings.SettingsEnum.ReturnType.STRING; + import android.content.Context; import android.util.Log; +import androidx.annotation.NonNull; + import app.revanced.tiktok.utils.LogHelper; import app.revanced.tiktok.utils.ReVancedUtils; -import app.revanced.tiktok.utils.SharedPrefHelper; public enum SettingsEnum { //TikTok Settings - TIK_REMOVE_ADS("tik_remove_ads", true, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN, true), - TIK_HIDE_LIVE("tik_hide_live", false, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN, true), - TIK_DOWN_PATH("tik_down_path", "DCIM/TikTok", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING), - TIK_DOWN_WATERMARK("tik_down_watermark", true, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN), - TIK_SIMSPOOF("tik_simspoof", true, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN, true), - TIK_SIMSPOOF_ISO("tik_simspoof_iso", "us", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING), - TIK_SIMSPOOF_MCCMNC("tik_simspoof_mccmnc", "310160", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING), - TIK_SIMSPOOF_OP_NAME("tik_simspoof_op_name", "T-Mobile", SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.STRING), - TIK_DEBUG("tik_debug", false, SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS, ReturnType.BOOLEAN); + TIK_DEBUG("tik_debug", BOOLEAN, FALSE), // must be first value, otherwise logging during loading will not work + TIK_REMOVE_ADS("tik_remove_ads", BOOLEAN, TRUE, true), + TIK_HIDE_LIVE("tik_hide_live", BOOLEAN, FALSE, true), + TIK_DOWN_PATH("tik_down_path", STRING, "DCIM/TikTok"), + TIK_DOWN_WATERMARK("tik_down_watermark", BOOLEAN, TRUE), + TIK_SIMSPOOF("tik_simspoof", BOOLEAN, TRUE, true), + TIK_SIMSPOOF_ISO("tik_simspoof_iso", STRING, "us"), + TIK_SIMSPOOF_MCCMNC("tik_simspoof_mccmnc", STRING, "310160"), + TIK_SIMSPOOF_OP_NAME("tik_simspoof_op_name", STRING, "T-Mobile"); static { - load(); + loadAllSettings(); } - private final String path; - private final Object defaultValue; - private final SharedPrefHelper.SharedPrefNames sharedPref; - private final ReturnType returnType; - private final boolean rebootApp; - private Object value = null; + @NonNull + public final String path; + @NonNull + public final Object defaultValue; + @NonNull + public final SharedPrefCategory sharedPref; + @NonNull + public final ReturnType returnType; + /** + * If the app should be rebooted, if this setting is changed + */ + public final boolean rebootApp; - SettingsEnum(String path, Object defaultValue, ReturnType returnType) { + private Object value; + + SettingsEnum(String path, ReturnType returnType, Object defaultValue) { + this(path, returnType, defaultValue, SharedPrefCategory.TIKTOK_PREFS, false); + } + SettingsEnum(String path, ReturnType returnType, Object defaultValue, boolean rebootApp) { + this(path, returnType, defaultValue, SharedPrefCategory.TIKTOK_PREFS, rebootApp); + } + SettingsEnum(@NonNull String path, @NonNull ReturnType returnType, @NonNull Object defaultValue, + @NonNull SharedPrefCategory prefName, boolean rebootApp) { this.path = path; - this.defaultValue = defaultValue; - this.sharedPref = SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS; this.returnType = returnType; - this.rebootApp = false; - } - - SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType) { - this.path = path; this.defaultValue = defaultValue; this.sharedPref = prefName; - this.returnType = returnType; - this.rebootApp = false; - } - - SettingsEnum(String path, Object defaultValue, SharedPrefHelper.SharedPrefNames prefName, ReturnType returnType, Boolean rebootApp) { - this.path = path; - this.defaultValue = defaultValue; - this.sharedPref = prefName; - this.returnType = returnType; this.rebootApp = rebootApp; } - private static void load() { - Context context = ReVancedUtils.getAppContext(); - if (context == null) { - Log.e("revanced: SettingsEnum", "Context returned null! Setings NOT initialized"); - } else { - try { - for (SettingsEnum setting : values()) { - Object value = setting.getDefaultValue(); - - //LogHelper is not initialized here - Log.d("revanced: SettingsEnum", "Loading Setting: " + setting.name()); - - switch (setting.getReturnType()) { - case FLOAT: - value = SharedPrefHelper.getFloat(context, setting.sharedPref, setting.getPath(), (float) setting.getDefaultValue()); - break; - case LONG: - value = SharedPrefHelper.getLong(context, setting.sharedPref, setting.getPath(), (long) setting.getDefaultValue()); - break; - case BOOLEAN: - value = SharedPrefHelper.getBoolean(context, setting.sharedPref, setting.getPath(), (boolean) setting.getDefaultValue()); - break; - case INTEGER: - value = SharedPrefHelper.getInt(context, setting.sharedPref, setting.getPath(), (int) setting.getDefaultValue()); - break; - case STRING: - value = SharedPrefHelper.getString(context, setting.sharedPref, setting.getPath(), (String) setting.getDefaultValue()); - break; - default: - LogHelper.printException(SettingsEnum.class, "Setting does not have a valid Type. Name is: " + setting.name()); - break; - } - setting.setValue(value); - - //LogHelper is not initialized here - Log.d("revanced: SettingsEnum", "Loaded Setting: " + setting.name() + " Value: " + value); - } - } catch (Throwable th) { - LogHelper.printException(SettingsEnum.class, "Error during load()!", th); + private static void loadAllSettings() { + try { + Context context = ReVancedUtils.getAppContext(); + if (context == null) { + Log.e("revanced: SettingsEnum", "Context returned null! Settings NOT initialized"); + return; } + for (SettingsEnum setting : values()) { + setting.load(context); + } + } catch (Exception ex) { + LogHelper.printException(SettingsEnum.class, "Error during load()!", ex); } } - public void setValue(Object newValue) { - this.value = newValue; + private void load(Context context) { + switch (returnType) { + case BOOLEAN: + value = sharedPref.getBoolean(context, path, (boolean) defaultValue); + break; + case INTEGER: + value = sharedPref.getInt(context, path, (Integer) defaultValue); + break; + case LONG: + value = sharedPref.getLong(context, path, (Long) defaultValue); + break; + case FLOAT: + value = sharedPref.getFloat(context, path, (Float) defaultValue); + break; + case STRING: + value = sharedPref.getString(context, path, (String) defaultValue); + break; + default: + throw new IllegalStateException(name()); + } + + LogHelper.debug(SettingsEnum.class, "Loaded Setting: " + name() + " Value: " + value); + } + + /** + * Sets, but does _not_ persistently save the value. + * + * This intentionally is a static method, to deter accidental usage + * when {@link #saveValue(Object)} was intended. + */ + public static void setValue(SettingsEnum setting, Object newValue) { + // FIXME: this should validate the parameter matches the return type + setting.value = newValue; } public void saveValue(Object newValue) { @@ -108,48 +116,39 @@ public enum SettingsEnum { return; } - if (returnType == ReturnType.BOOLEAN) { - SharedPrefHelper.saveBoolean(context, sharedPref, path, (Boolean) newValue); + if (returnType == BOOLEAN) { + sharedPref.saveBoolean(context, path, (Boolean) newValue); } else { - SharedPrefHelper.saveString(context, sharedPref, path, newValue + ""); + sharedPref.saveString(context, path, newValue.toString()); } value = newValue; } - public int getInt() { - return (int) value; - } - - public String getString() { - return (String) value; - } - public boolean getBoolean() { return (Boolean) value; } - public Long getLong() { + public int getInt() { + return (Integer) value; + } + + public long getLong() { return (Long) value; } - public Float getFloat() { + public float getFloat() { return (Float) value; } - public Object getDefaultValue() { - return defaultValue; + public String getString() { + return (String) value; } - public String getPath() { - return path; + public enum ReturnType { + BOOLEAN, + INTEGER, + LONG, + FLOAT, + STRING, } - - public ReturnType getReturnType() { - return returnType; - } - - public boolean shouldRebootOnChange() { - return rebootApp; - } - } diff --git a/app/src/main/java/app/revanced/tiktok/settings/SharedPrefCategory.java b/app/src/main/java/app/revanced/tiktok/settings/SharedPrefCategory.java new file mode 100644 index 00000000..79160ba1 --- /dev/null +++ b/app/src/main/java/app/revanced/tiktok/settings/SharedPrefCategory.java @@ -0,0 +1,71 @@ +package app.revanced.tiktok.settings; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; + +public enum SharedPrefCategory { + TIKTOK_PREFS("tiktok_revanced"); + + @NonNull + public final String prefName; + + SharedPrefCategory(@NonNull String prefName) { + this.prefName = prefName; + } + + @NonNull + @Override + public String toString() { + return prefName; + } + + private SharedPreferences getPreferences(Context context) { + return context.getSharedPreferences(prefName, Context.MODE_PRIVATE); + } + + public void saveBoolean(Context context, String key, boolean value) { + getPreferences(context).edit().putBoolean(key, value).apply(); + } + + public void saveString(Context context, String key, String value) { + getPreferences(context).edit().putString(key, value).apply(); + } + + public boolean getBoolean(Context context, String key, boolean _default) { + return getPreferences(context).getBoolean(key, _default); + } + + public Integer getInt(Context context, String key, Integer _default) { + SharedPreferences sharedPreferences = getPreferences(context); + try { + return Integer.valueOf(sharedPreferences.getString(key, _default.toString())); + } catch (ClassCastException ex) { + return sharedPreferences.getInt(key, _default); + } + } + + public Long getLong(Context context, String key, Long _default) { + SharedPreferences sharedPreferences = getPreferences(context); + try { + return Long.valueOf(sharedPreferences.getString(key, _default.toString())); + } catch (ClassCastException ex) { + return sharedPreferences.getLong(key, _default); + } + } + + public Float getFloat(Context context, String key, Float _default) { + SharedPreferences sharedPreferences = getPreferences(context); + try { + return Float.valueOf(sharedPreferences.getString(key, _default.toString())); + } catch (ClassCastException ex) { + return sharedPreferences.getFloat(key, _default); + } + } + + public String getString(Context context, String key, String _default) { + return getPreferences(context).getString(key, _default); + } + +} diff --git a/app/src/main/java/app/revanced/tiktok/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/tiktok/settingsmenu/ReVancedSettingsFragment.java index bb66c254..0febb987 100644 --- a/app/src/main/java/app/revanced/tiktok/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/tiktok/settingsmenu/ReVancedSettingsFragment.java @@ -21,9 +21,9 @@ import androidx.annotation.Nullable; import com.ss.android.ugc.aweme.splash.SplashActivity; import app.revanced.tiktok.settings.SettingsEnum; +import app.revanced.tiktok.settings.SharedPrefCategory; import app.revanced.tiktok.settingsmenu.preference.DownloadPathPreference; import app.revanced.tiktok.utils.ReVancedUtils; -import app.revanced.tiktok.utils.SharedPrefHelper; public class ReVancedSettingsFragment extends PreferenceFragment { @@ -32,9 +32,9 @@ public class ReVancedSettingsFragment extends PreferenceFragment { SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { for (SettingsEnum setting : SettingsEnum.values()) { - if (!setting.getPath().equals(str)) continue; + if (!setting.path.equals(str)) continue; - if (ReVancedUtils.getAppContext() != null && this.settingsInitialized && setting.shouldRebootOnChange()) { + if (ReVancedUtils.getAppContext() != null && this.settingsInitialized && setting.rebootApp) { rebootDialog(getActivity()); } } @@ -43,7 +43,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.TIKTOK_PREFS.getName()); + getPreferenceManager().setSharedPreferencesName(SharedPrefCategory.TIKTOK_PREFS.prefName); getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this.listener); this.Registered = true; @@ -61,12 +61,14 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { SwitchPreference preference = new SwitchPreference(context); feedFilter.addPreference(preference); - preference.setKey(SettingsEnum.TIK_REMOVE_ADS.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_REMOVE_ADS.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_REMOVE_ADS.path); + preference.setDefaultValue(SettingsEnum.TIK_REMOVE_ADS.defaultValue); preference.setChecked(SettingsEnum.TIK_REMOVE_ADS.getBoolean()); preference.setTitle("Remove feed ads"); preference.setSummary("Remove ads from feed."); preference.setOnPreferenceChangeListener((pref, newValue) -> { + // FIXME: the value is already saved in the preferences. + // instead of saving again, simple call SettingsEnum#setValue() final boolean value = (Boolean) newValue; SettingsEnum.TIK_REMOVE_ADS.saveValue(value); return true; @@ -76,8 +78,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { SwitchPreference preference = new SwitchPreference(context); feedFilter.addPreference(preference); - preference.setKey(SettingsEnum.TIK_HIDE_LIVE.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_HIDE_LIVE.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_HIDE_LIVE.path); + preference.setDefaultValue(SettingsEnum.TIK_HIDE_LIVE.defaultValue); preference.setChecked(SettingsEnum.TIK_HIDE_LIVE.getBoolean()); preference.setTitle("Hide livestreams"); preference.setSummary("Hide livestreams from feed."); @@ -98,8 +100,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { DownloadPathPreference preference = new DownloadPathPreference(context); download.addPreference(preference); - preference.setKey(SettingsEnum.TIK_DOWN_PATH.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_DOWN_PATH.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_DOWN_PATH.path); + preference.setDefaultValue(SettingsEnum.TIK_DOWN_PATH.defaultValue); preference.setValue(SettingsEnum.TIK_DOWN_PATH.getString()); preference.setTitle("Download path"); preference.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + preference.getValue()); @@ -113,8 +115,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { SwitchPreference preference = new SwitchPreference(context); download.addPreference(preference); - preference.setKey(SettingsEnum.TIK_DOWN_WATERMARK.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_DOWN_WATERMARK.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_DOWN_WATERMARK.path); + preference.setDefaultValue(SettingsEnum.TIK_DOWN_WATERMARK.defaultValue); preference.setChecked(SettingsEnum.TIK_DOWN_WATERMARK.getBoolean()); preference.setTitle("Remove watermark"); preference.setOnPreferenceChangeListener((pref, newValue) -> { @@ -134,8 +136,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { SwitchPreference preference = new SwitchPreference(context); simSpoof.addPreference(preference); - preference.setKey(SettingsEnum.TIK_SIMSPOOF.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_SIMSPOOF.path); + preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF.defaultValue); preference.setChecked(SettingsEnum.TIK_SIMSPOOF.getBoolean()); preference.setTitle("Fake sim card info"); preference.setSummary("Bypass regional restriction by fake sim card information."); @@ -149,8 +151,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { EditTextPreference preference = new EditTextPreference(context); simSpoof.addPreference(preference); - preference.setKey(SettingsEnum.TIK_SIMSPOOF_ISO.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_ISO.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_SIMSPOOF_ISO.path); + preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_ISO.defaultValue); preference.setText(SettingsEnum.TIK_SIMSPOOF_ISO.getString()); preference.setTitle("Country ISO"); preference.setSummary("us, uk, jp, ..."); @@ -164,8 +166,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { EditTextPreference preference = new EditTextPreference(context); simSpoof.addPreference(preference); - preference.setKey(SettingsEnum.TIK_SIMSPOOF_MCCMNC.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_MCCMNC.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_SIMSPOOF_MCCMNC.path); + preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_MCCMNC.defaultValue); preference.setText(SettingsEnum.TIK_SIMSPOOF_MCCMNC.getString()); preference.setTitle("Operator mcc+mnc"); preference.setSummary("mcc+mnc"); @@ -179,8 +181,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { EditTextPreference preference = new EditTextPreference(context); simSpoof.addPreference(preference); - preference.setKey(SettingsEnum.TIK_SIMSPOOF_OP_NAME.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_OP_NAME.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_SIMSPOOF_OP_NAME.path); + preference.setDefaultValue(SettingsEnum.TIK_SIMSPOOF_OP_NAME.defaultValue); preference.setText(SettingsEnum.TIK_SIMSPOOF_OP_NAME.getString()); preference.setTitle("Operator name"); preference.setSummary("Name of the operator"); @@ -200,8 +202,8 @@ public class ReVancedSettingsFragment extends PreferenceFragment { { SwitchPreference preference = new SwitchPreference(context); integration.addPreference(preference); - preference.setKey(SettingsEnum.TIK_DEBUG.getPath()); - preference.setDefaultValue(SettingsEnum.TIK_DEBUG.getDefaultValue()); + preference.setKey(SettingsEnum.TIK_DEBUG.path); + preference.setDefaultValue(SettingsEnum.TIK_DEBUG.defaultValue); preference.setChecked(SettingsEnum.TIK_DEBUG.getBoolean()); preference.setTitle("Enable debug log"); preference.setSummary("Show integration debug log."); diff --git a/app/src/main/java/app/revanced/tiktok/utils/SharedPrefHelper.java b/app/src/main/java/app/revanced/tiktok/utils/SharedPrefHelper.java deleted file mode 100644 index ff9dd0ec..00000000 --- a/app/src/main/java/app/revanced/tiktok/utils/SharedPrefHelper.java +++ /dev/null @@ -1,82 +0,0 @@ -package app.revanced.tiktok.utils; - -import android.content.Context; -import android.content.SharedPreferences; - -public class SharedPrefHelper { - public static void saveString(Context context, SharedPrefNames prefName, String key, String value) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - sharedPreferences.edit().putString(key, value).apply(); - } - - public static void saveBoolean(Context context, SharedPrefNames prefName, String key, Boolean value) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - sharedPreferences.edit().putBoolean(key, value).apply(); - } - - public static String getString(Context context, SharedPrefNames prefName, String key, String _default) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - return (sharedPreferences.getString(key, _default)); - } - - public static Boolean getBoolean(Context context, SharedPrefNames prefName, String key, Boolean _default) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - return (sharedPreferences.getBoolean(key, _default)); - } - - public static Long getLong(Context context, SharedPrefNames prefName, String key, Long _default) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - try { - return Long.valueOf(sharedPreferences.getString(key, _default + "")); - } catch (ClassCastException ex) { - return sharedPreferences.getLong(key, _default); - } - } - - public static Float getFloat(Context context, SharedPrefNames prefName, String key, Float _default) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - try { - return Float.valueOf(sharedPreferences.getString(key, _default + "")); - } catch (ClassCastException ex) { - return sharedPreferences.getFloat(key, _default); - } - } - - public static Integer getInt(Context context, SharedPrefNames prefName, String key, Integer _default) { - SharedPreferences sharedPreferences = getPreferences(context, prefName); - try { - return Integer.valueOf(sharedPreferences.getString(key, _default + "")); - } catch (ClassCastException ex) { - return sharedPreferences.getInt(key, _default); - } - } - - public static SharedPreferences getPreferences(Context context, SharedPrefNames name) { - if (context == null) return null; - return context.getSharedPreferences(name.getName(), Context.MODE_PRIVATE); - } - - public static SharedPreferences getPreferences(Context context, String name) { - if (context == null) return null; - return context.getSharedPreferences(name, Context.MODE_PRIVATE); - } - - public enum SharedPrefNames { - TIKTOK_PREFS("tiktok_revanced"); - - private final String name; - - SharedPrefNames(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } - } -} diff --git a/app/src/main/java/app/revanced/twitch/settings/ReturnType.java b/app/src/main/java/app/revanced/twitch/settings/ReturnType.java deleted file mode 100644 index 142c4a1f..00000000 --- a/app/src/main/java/app/revanced/twitch/settings/ReturnType.java +++ /dev/null @@ -1,5 +0,0 @@ -package app.revanced.twitch.settings; - -public enum ReturnType { - BOOLEAN, INTEGER, STRING, LONG, FLOAT -} diff --git a/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java b/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java index fdb48ba9..9103f654 100644 --- a/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java @@ -1,150 +1,163 @@ package app.revanced.twitch.settings; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.twitch.settings.SettingsEnum.ReturnType.BOOLEAN; +import static app.revanced.twitch.settings.SettingsEnum.ReturnType.STRING; + import android.content.Context; import android.content.SharedPreferences; +import androidx.annotation.NonNull; + import app.revanced.twitch.utils.LogHelper; import app.revanced.twitch.utils.ReVancedUtils; public enum SettingsEnum { /* Ads */ - BLOCK_VIDEO_ADS("revanced_block_video_ads", true, ReturnType.BOOLEAN), - BLOCK_AUDIO_ADS("revanced_block_audio_ads", true, ReturnType.BOOLEAN), - BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", "ttv-lol", ReturnType.STRING), + BLOCK_VIDEO_ADS("revanced_block_video_ads", BOOLEAN, TRUE), + BLOCK_AUDIO_ADS("revanced_block_audio_ads", BOOLEAN, TRUE), + BLOCK_EMBEDDED_ADS("revanced_block_embedded_ads", STRING, "ttv-lol"), /* Chat */ - SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", "cross-out", ReturnType.STRING), + SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", STRING, "cross-out"), /* Misc */ - DEBUG_MODE("revanced_debug_mode", false, ReturnType.BOOLEAN, true); + DEBUG_MODE("revanced_debug_mode", BOOLEAN, FALSE, true); public static final String REVANCED_PREFS = "revanced_prefs"; - private final String path; - private final Object defaultValue; - private final ReturnType returnType; - private final boolean rebootApp; + @NonNull + public final String path; + @NonNull + public final ReturnType returnType; + @NonNull + public final Object defaultValue; + /** + * If the app should be rebooted, if this setting is changed + */ + public final boolean rebootApp; - private Object value = null; + private Object value; - SettingsEnum(String path, Object defaultValue, ReturnType returnType) { - this.path = path; - this.defaultValue = defaultValue; - this.returnType = returnType; - this.rebootApp = false; + SettingsEnum(String path, ReturnType returnType, Object defaultValue) { + this(path, returnType, defaultValue, false); } - SettingsEnum(String path, Object defaultValue, ReturnType returnType, Boolean rebootApp) { + SettingsEnum(@NonNull String path, @NonNull ReturnType returnType, @NonNull Object defaultValue, boolean rebootApp) { this.path = path; - this.defaultValue = defaultValue; this.returnType = returnType; + this.defaultValue = defaultValue; this.rebootApp = rebootApp; } static { - load(); + loadAllSettings(); } - private static void load() { + private static void loadAllSettings() { ReVancedUtils.ifContextAttached((context -> { try { SharedPreferences prefs = context.getSharedPreferences(REVANCED_PREFS, Context.MODE_PRIVATE); for (SettingsEnum setting : values()) { - Object value = setting.getDefaultValue(); - - try { - switch (setting.getReturnType()) { - case BOOLEAN: - value = prefs.getBoolean(setting.getPath(), (boolean)setting.getDefaultValue()); - break; - // Numbers are implicitly converted from strings - case FLOAT: - case LONG: - case INTEGER: - case STRING: - value = prefs.getString(setting.getPath(), setting.getDefaultValue() + ""); - break; - default: - LogHelper.error("Setting '%s' does not have a valid type", setting.name()); - break; - } - } - catch (ClassCastException ex) { - LogHelper.printException("Failed to read value", ex); - } - - setting.setValue(value); - LogHelper.debug("Loaded setting '%s' with value %s", setting.name(), value); + setting.load(prefs); } - } catch (Throwable th) { - LogHelper.printException("Failed to load settings", th); + } catch (Exception ex) { + LogHelper.printException("Failed to load settings", ex); } })); } - public void setValue(Object newValue) { + private void load(SharedPreferences prefs) { + try { + switch (returnType) { + case BOOLEAN: + setValue(prefs.getBoolean(path, (Boolean) defaultValue)); + break; + // Numbers are implicitly converted from strings + case INTEGER: + case LONG: + case FLOAT: + case STRING: + setValue(prefs.getString(path, defaultValue.toString())); + break; + default: + throw new IllegalStateException(name()); + } + LogHelper.debug("Loaded setting '%s' with value %s", name(), value); + } catch (ClassCastException ex) { + LogHelper.printException("Failed to read value", ex); + } + } + + /** + * Sets, but does _not_ persistently save the value. + * + * This intentionally is a static method, to deter accidental usage + * when {@link #saveValue(Object)} was intended. + */ + public static void setValue(SettingsEnum setting, Object newValue) { + setting.setValue(newValue); + } + + private void setValue(Object newValue) { // Implicitly convert strings to numbers depending on the ResultType switch (returnType) { case FLOAT: - value = Float.valueOf(newValue + ""); + value = Float.valueOf(newValue.toString()); break; case LONG: - value = Long.valueOf(newValue + ""); + value = Long.valueOf(newValue.toString()); break; case INTEGER: - value = Integer.valueOf(newValue + ""); + value = Integer.valueOf(newValue.toString()); break; - default: + case BOOLEAN: + case STRING: value = newValue; break; + default: + throw new IllegalArgumentException(name()); } } public void saveValue(Object newValue) { ReVancedUtils.ifContextAttached((context) -> { SharedPreferences prefs = context.getSharedPreferences(REVANCED_PREFS, Context.MODE_PRIVATE); - if (returnType == ReturnType.BOOLEAN) { + if (returnType == BOOLEAN) { prefs.edit().putBoolean(path, (Boolean)newValue).apply(); } else { - prefs.edit().putString(path, newValue + "").apply(); + prefs.edit().putString(path, newValue.toString()).apply(); } - value = newValue; + setValue(newValue); }); } - public int getInt() { - return (int) value; - } - - public String getString() { - return (String) value; - } - public boolean getBoolean() { return (Boolean) value; } - public Long getLong() { + public int getInt() { + return (Integer) value; + } + + public long getLong() { return (Long) value; } - public Float getFloat() { + public float getFloat() { return (Float) value; } - public Object getDefaultValue() { - return defaultValue; + public String getString() { + return (String) value; } - public String getPath() { - return path; - } - - public ReturnType getReturnType() { - return returnType; - } - - public boolean shouldRebootOnChange() { - return rebootApp; + public enum ReturnType { + BOOLEAN, + INTEGER, + LONG, + FLOAT, + STRING, } } diff --git a/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java index e18246d3..1da3eacd 100644 --- a/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/twitch/settingsmenu/ReVancedSettingsFragment.java @@ -22,7 +22,6 @@ import androidx.annotation.Nullable; import app.revanced.twitch.settings.SettingsEnum; import app.revanced.twitch.utils.LogHelper; import app.revanced.twitch.utils.ReVancedUtils; - import tv.twitch.android.app.core.LandingActivity; public class ReVancedSettingsFragment extends PreferenceFragment { @@ -41,28 +40,28 @@ public class ReVancedSettingsFragment extends PreferenceFragment { */ private void syncPreference(@Nullable String key) { for (SettingsEnum setting : SettingsEnum.values()) { - if (!setting.getPath().equals(key) && key != null) + if (!setting.path.equals(key) && key != null) continue; - Preference pref = this.findPreference(setting.getPath()); - LogHelper.debug("Syncing setting '%s' with UI", setting.getPath()); + Preference pref = this.findPreference(setting.path); + LogHelper.debug("Syncing setting '%s' with UI", setting.path); if (pref instanceof SwitchPreference) { - setting.setValue(((SwitchPreference) pref).isChecked()); + SettingsEnum.setValue(setting, ((SwitchPreference) pref).isChecked()); } else if (pref instanceof EditTextPreference) { - setting.setValue(((EditTextPreference) pref).getText()); + SettingsEnum.setValue(setting, ((EditTextPreference) pref).getText()); } else if (pref instanceof ListPreference) { ListPreference listPref = (ListPreference) pref; listPref.setSummary(listPref.getEntry()); - setting.setValue(listPref.getValue()); + SettingsEnum.setValue(setting, listPref.getValue()); } else { LogHelper.error("Setting '%s' cannot be handled!", pref); } - if (ReVancedUtils.getContext() != null && key != null && settingsInitialized && setting.shouldRebootOnChange()) { + if (ReVancedUtils.getContext() != null && key != null && settingsInitialized && setting.rebootApp) { rebootDialog(getActivity()); } From 584de16236ff758c2067ee84ba4cc04d765d49ba Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:28:12 +0400 Subject: [PATCH 09/30] fix(youtube/settings): fix dialog not shown if dismissed with back button --- .../settingsmenu/ReVancedSettingsFragment.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index 43a240dd..f8ce46a0 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -32,7 +32,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment { /** * If a dialog is currently being shown. Used to prevent showing additional dialogs if user cancels a dialog. */ - private static boolean currentlyShowingDialog; + private boolean currentlyShowingDialog; SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { @@ -136,6 +136,9 @@ public class ReVancedSettingsFragment extends PreferenceFragment { }) .setNegativeButton(negativeButton, (dialog, id) -> { currentlyShowingDialog = false; + }) + .setOnDismissListener((dialog) -> { + currentlyShowingDialog = false; }).show(); } @@ -156,7 +159,9 @@ public class ReVancedSettingsFragment extends PreferenceFragment { SettingsEnum.setValue(setting, defaultBooleanValue); switchPref.setChecked(defaultBooleanValue); currentlyShowingDialog = false; + }) + .setOnDismissListener((dialog) -> { + currentlyShowingDialog = false; }).show(); } - } From 80ae9ebbd241fa4931124d8dae0e3124e97b4da3 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:40:53 +0400 Subject: [PATCH 10/30] refactor(youtube/settings): remove code that is no longer needed --- .../ReVancedSettingsFragment.java | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index f8ce46a0..8862f88e 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -29,11 +29,6 @@ import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public class ReVancedSettingsFragment extends PreferenceFragment { - /** - * If a dialog is currently being shown. Used to prevent showing additional dialogs if user cancels a dialog. - */ - private boolean currentlyShowingDialog; - SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { SettingsEnum setting = SettingsEnum.settingFromPath(str); @@ -70,12 +65,10 @@ public class ReVancedSettingsFragment extends PreferenceFragment { LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref); } - if (!currentlyShowingDialog) { - if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { - showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting); - } else if (setting.rebootApp) { - rebootDialog(getActivity()); - } + if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { + showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting); + } else if (setting.rebootApp) { + rebootDialog(getActivity()); } enableDisablePreferences(); @@ -126,42 +119,30 @@ public class ReVancedSettingsFragment extends PreferenceFragment { } private void rebootDialog(@NonNull Activity activity) { - currentlyShowingDialog = true; String positiveButton = str("in_app_update_restart_button"); String negativeButton = str("sign_in_cancel"); new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")) .setPositiveButton(positiveButton, (dialog, id) -> { reboot(activity); - currentlyShowingDialog = false; }) - .setNegativeButton(negativeButton, (dialog, id) -> { - currentlyShowingDialog = false; - }) - .setOnDismissListener((dialog) -> { - currentlyShowingDialog = false; - }).show(); + .setNegativeButton(negativeButton, null) + .show(); } private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) { - currentlyShowingDialog = true; new AlertDialog.Builder(activity) .setTitle(str("revanced_settings_confirm_user_dialog_title")) .setMessage(setting.userDialogMessage.toString()) .setPositiveButton(android.R.string.ok, (dialog, id) -> { if (setting.rebootApp) { rebootDialog(activity); - } else { - currentlyShowingDialog = false; } }) .setNegativeButton(android.R.string.cancel, (dialog, id) -> { Boolean defaultBooleanValue = (Boolean) setting.defaultValue; SettingsEnum.setValue(setting, defaultBooleanValue); switchPref.setChecked(defaultBooleanValue); - currentlyShowingDialog = false; }) - .setOnDismissListener((dialog) -> { - currentlyShowingDialog = false; - }).show(); + .show(); } } From e3d923d564ae572c5e0e10a86ce17b8009ec8c42 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 7 Apr 2023 23:16:26 +0400 Subject: [PATCH 11/30] fix(youtube/return-youtube-dislike): fix error toast when voting (#349) --- .../integrations/returnyoutubedislike/ReturnYouTubeDislike.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index 26b0b9aa..d9a41374 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -388,7 +388,7 @@ public class ReturnYouTubeDislike { ReVancedUtils.verifyOffMainThread(); String userId = SettingsEnum.RYD_USER_ID.getString(); - if (userId != null) { + if (!userId.isEmpty()) { return userId; } From 6dbccfd472d843b5c3f0efed39b575d3ea7ac04f Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 8 Apr 2023 19:43:38 +0400 Subject: [PATCH 12/30] fix(youtube/sponsorblock): settings do not show default behavior (#351) --- .../sponsorblock/objects/SegmentCategoryListPreference.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java index 829b2894..b1ab4683 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java @@ -31,6 +31,7 @@ public class SegmentCategoryListPreference extends ListPreference { super(context); this.category = Objects.requireNonNull(category); setKey(category.key); + setDefaultValue(category.behaviour.key); setEntries(CategoryBehaviour.getBehaviorNames()); setEntryValues(CategoryBehaviour.getBehaviorKeys()); setSummary(category.description.toString()); From 6265a91841f8b037be22dd5fd5399dbeeb666745 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 9 Apr 2023 20:13:06 +0200 Subject: [PATCH 13/30] feat(youtube/general-ads): block new type of ad --- .../app/revanced/integrations/patches/GeneralAdsPatch.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index eb5a988c..96c41907 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -64,7 +64,8 @@ public final class GeneralAdsPatch extends Filter { "carousel_footered_layout", "text_image_button_layout", "primetime_promo", - "feature_grid_interstitial" + "feature_grid_interstitial", + "product_details" ); var movieAds = new BlockRule( SettingsEnum.ADREMOVER_MOVIE_REMOVAL, From b959c8ef98e869201c8bc2609943108283fea453 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:59:42 +0400 Subject: [PATCH 14/30] fix(youtube/sponsorblock): change default behavior to better match the browser (#353) --- .../integrations/sponsorblock/objects/SegmentCategory.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java index a11703da..f5f0da4f 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java @@ -3,6 +3,7 @@ package app.revanced.integrations.sponsorblock.objects; import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.IGNORE; import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP; import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; +import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE; import static app.revanced.integrations.utils.StringRef.sf; import android.content.SharedPreferences; @@ -27,11 +28,11 @@ import app.revanced.integrations.utils.StringRef; public enum SegmentCategory { SPONSOR("sponsor", sf("sb_segments_sponsor"), sf("sb_segments_sponsor_sum"), sf("sb_skip_button_sponsor"), sf("sb_skipped_sponsor"), - SKIP_AUTOMATICALLY, 0x00D400), + SKIP_AUTOMATICALLY_ONCE, 0x00D400), SELF_PROMO("selfpromo", sf("sb_segments_selfpromo"), sf("sb_segments_selfpromo_sum"), sf("sb_skip_button_selfpromo"), sf("sb_skipped_selfpromo"), - SKIP_AUTOMATICALLY, 0xFFFF00), + MANUAL_SKIP, 0xFFFF00), INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"), - SKIP_AUTOMATICALLY, 0xCC00FF), + MANUAL_SKIP, 0xCC00FF), INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"), sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"), sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"), From 212e4f2ce43360776fe20467c4142c9936b22d42 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 12 Apr 2023 21:52:45 +0400 Subject: [PATCH 15/30] fix(youtube/return-youtube-dislike): stale dislike data shown after opening / closing the app during shorts playback (#356) --- .../patches/ReturnYouTubeDislikePatch.java | 2 +- .../ReturnYouTubeDislike.java | 128 +++++++++--------- .../ReturnYouTubeDislikeSettingsFragment.java | 4 +- 3 files changed, 69 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java index 69a5def1..dd1d6e37 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -6,7 +6,7 @@ import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import java.util.concurrent.atomic.AtomicReference; /** - * Used by app.revanced.patches.youtube.layout.returnyoutubedislike.patch.ReturnYouTubeDislikePatch + * TODO: delete this empty class, and point the patch to {@link ReturnYouTubeDislike} */ public class ReturnYouTubeDislikePatch { diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index d9a41374..d441c2b1 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -97,7 +97,7 @@ public class ReturnYouTubeDislike { */ @Nullable @GuardedBy("videoIdLockObject") - private static Spanned replacementLikeDislikeSpan; + private static SpannableString replacementLikeDislikeSpan; public enum Vote { LIKE(1), @@ -147,6 +147,18 @@ public class ReturnYouTubeDislike { } } + /** + * Should be called if user changes settings for dislikes appearance. + */ + public static void clearCache() { + synchronized (videoIdLockObject) { + if (replacementLikeDislikeSpan != null) { + LogHelper.printDebug(() -> "Clearing cache"); + } + replacementLikeDislikeSpan = null; + } + } + @Nullable private static String getCurrentVideoId() { synchronized (videoIdLockObject) { @@ -184,6 +196,12 @@ public class ReturnYouTubeDislike { } LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType); setCurrentVideoId(videoId); + + // If a Short is opened while a regular video is on screen, this will incorrectly set this as false. + // But this check is needed to fix unusual situations of opening/closing the app + // while both a regular video and a short are on screen. + lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden(); + // no need to wrap the call in a try/catch, // as any exceptions are propagated out in the later Future#Get call voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); @@ -194,15 +212,18 @@ public class ReturnYouTubeDislike { } /** + * Called when a litho text component is created. + * * This method is sometimes called on the main thread, but it usually is called _off_ the main thread. * This method can be called multiple times for the same UI element (including after dislikes was added) + * + * @param textRef atomic reference should always be non null, but the spanned reference inside can be null. */ public static void onComponentCreated(@NonNull Object conversionContext, @NonNull AtomicReference textRef) { try { if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; - // do not set lastVideoLoadedWasShort to false. It will be cleared when the next regular video is loaded. - if (lastVideoLoadedWasShort || PlayerType.getCurrent().isNoneOrHidden()) { + if (PlayerType.getCurrent().isNoneOrHidden()) { return; } @@ -216,6 +237,16 @@ public class ReturnYouTubeDislike { return; } + if (lastVideoLoadedWasShort) { + // user: + // 1, opened a video + // 2. opened a short (without closing the regular video) + // 3. closed the short + // 4. regular video is now present, but the videoId and RYD data is still for the short + LogHelper.printDebug(() -> "Ignoring onComponentCreated(), as data loaded is is for prior short"); + return; + } + Spanned replacement = waitForFetchAndUpdateReplacementSpan((Spanned) textRef.get(), isSegmentedButton); if (replacement != null) { textRef.set(replacement); @@ -225,11 +256,14 @@ public class ReturnYouTubeDislike { } } - public static Spanned onShortsComponentCreated(Spanned span) { + /** + * Called when a Shorts dislike Spannable is created. + */ + public static Spanned onShortsComponentCreated(Spanned original) { try { if (SettingsEnum.RYD_ENABLED.getBoolean()) { - lastVideoLoadedWasShort = true; - Spanned replacement = waitForFetchAndUpdateReplacementSpan(span, false); + lastVideoLoadedWasShort = true; // it's now certain the video and data are a short + Spanned replacement = waitForFetchAndUpdateReplacementSpan(original, false); if (replacement != null) { return replacement; } @@ -237,7 +271,7 @@ public class ReturnYouTubeDislike { } catch (Exception ex) { LogHelper.printException(() -> "onShortsComponentCreated failure", ex); } - return span; + return original; } // alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick @@ -249,31 +283,27 @@ public class ReturnYouTubeDislike { * @return NULL if the span does not need changing or if RYD is not available */ @Nullable - private static Spanned waitForFetchAndUpdateReplacementSpan(@Nullable Spanned oldSpannable, boolean isSegmentedButton) { + private static SpannableString waitForFetchAndUpdateReplacementSpan(@Nullable Spanned oldSpannable, boolean isSegmentedButton) { if (oldSpannable == null) { LogHelper.printDebug(() -> "Cannot add dislikes (injection code was called with null Span)"); return null; } - // Must block the current thread until fetching is done - // There's no known way to edit the text after creation yet - long fetchStartTime = 0; try { synchronized (videoIdLockObject) { if (oldSpannable.equals(replacementLikeDislikeSpan)) { - LogHelper.printDebug(() -> "Ignoring previously created dislike span"); + LogHelper.printDebug(() -> "Ignoring span that already contains dislikes"); return null; } + if (replacementLikeDislikeSpan != null) { + LogHelper.printDebug(() -> "Using previously created dislike span"); + return replacementLikeDislikeSpan; + } if (isSegmentedButton) { if (isPreviouslyCreatedSegmentedSpan(oldSpannable)) { // need to recreate using original, as oldSpannable has prior outdated dislike values oldSpannable = originalDislikeSpan; if (oldSpannable == null) { - // Regular video is opened, then a short is opened then closed, - // then the app is closed then reopened (causes a call of NewVideoId() of the original videoId) - // The original video (that was opened the entire time), is still showing the dislikes count - // but the oldSpannable is now null because it was reset when the videoId was set again - LogHelper.printDebug(() -> "Cannot add dislikes - original span is null" + - " (short was opened/closed, then app was closed/opened?) "); // ignore, with no toast + LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen return null; } } else { @@ -282,21 +312,20 @@ public class ReturnYouTubeDislike { } } + // Must block the current thread until fetching is done + // There's no known way to edit the text after creation yet Future fetchFuture = getVoteFetchFuture(); if (fetchFuture == null) { LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)"); return null; } - if (SettingsEnum.DEBUG.getBoolean() && !fetchFuture.isDone()) { - fetchStartTime = System.currentTimeMillis(); - } RYDVoteData votingData = fetchFuture.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS); if (votingData == null) { LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); return null; } - Spanned replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData); + SpannableString replacement = createDislikeSpan(oldSpannable, isSegmentedButton, votingData); synchronized (videoIdLockObject) { replacementLikeDislikeSpan = replacement; } @@ -306,13 +335,16 @@ public class ReturnYouTubeDislike { } catch (TimeoutException e) { LogHelper.printDebug(() -> "UI timed out while waiting for fetch votes to complete"); // show no toast } catch (Exception e) { - LogHelper.printException(() -> "createReplacementSpan failure", e); // should never happen - } finally { - recordTimeUISpentWaitingForNetworkCall(fetchStartTime); + LogHelper.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen } return null; } + /** + * Called when the like/dislike button is clicked. + * + * @param vote int that matches {@link Vote#value} + */ public static void sendVote(int vote) { if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; @@ -335,11 +367,12 @@ public class ReturnYouTubeDislike { try { // Must make a local copy of videoId, since it may change between now and when the vote thread runs String videoIdToVoteFor = getCurrentVideoId(); - if (videoIdToVoteFor == null || (lastVideoLoadedWasShort && !PlayerType.getCurrent().isNoneOrHidden())) { + if (videoIdToVoteFor == null || lastVideoLoadedWasShort != PlayerType.getCurrent().isNoneOrHidden()) { // User enabled RYD after starting playback of a video. - // Or shorts was loaded with regular video present, then shorts was closed, and then user voted on the now visible original video - LogHelper.printException(() -> "Cannot send vote", - null, str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted")); + // Or shorts was loaded with regular video present, then shorts was closed, + // and then user voted on the now visible original video + // Cannot send a vote, because the loaded videoId is for the wrong video. + ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted")); return; } @@ -354,9 +387,7 @@ public class ReturnYouTubeDislike { } }); - synchronized (videoIdLockObject) { - replacementLikeDislikeSpan = null; // ui values need updating - } + clearCache(); // ui values need updating // update the downloaded vote data Future future = getVoteFetchFuture(); @@ -402,7 +433,7 @@ public class ReturnYouTubeDislike { /** * @param isSegmentedButton if UI is using the segmented single UI component for both like and dislike */ - private static Spanned createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) { + private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) { if (!isSegmentedButton) { // simple replacement of 'dislike' with a number/percentage return newSpannableWithDislikes(oldSpannable, voteData); @@ -493,14 +524,14 @@ public class ReturnYouTubeDislike { return false; } - private static Spannable newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) { + private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) { return newSpanUsingStylingOfAnotherSpan(sourceStyling, SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean() ? formatDislikePercentage(voteData.getDislikePercentage()) : formatDislikeCount(voteData.getDislikeCount())); } - private static Spannable newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) { + private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull String newSpanText) { SpannableString destination = new SpannableString(newSpanText); Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class); for (Object span : spans) { @@ -544,33 +575,6 @@ public class ReturnYouTubeDislike { return dislikePercentageFormatter.format(dislikePercentage); } } - - - /** - * Number of times the UI was forced to wait on a network fetch to complete - */ - private static volatile int numberOfTimesUIWaitedOnNetworkCalls; - - /** - * Total time the UI waited, of all times it was forced to wait. - */ - private static volatile long totalTimeUIWaitedOnNetworkCalls; - - @SuppressWarnings("NonAtomicOperationOnVolatileField") - private static void recordTimeUISpentWaitingForNetworkCall(long timeUIWaitStarted) { - if (timeUIWaitStarted == 0 || !SettingsEnum.DEBUG.getBoolean()) { - return; - } - final long timeUIWaitingTotal = System.currentTimeMillis() - timeUIWaitStarted; - LogHelper.printDebug(() -> "UI thread waited for: " + timeUIWaitingTotal + "ms for vote fetch to complete"); - - totalTimeUIWaitedOnNetworkCalls += timeUIWaitingTotal; - numberOfTimesUIWaitedOnNetworkCalls++; - final long averageTimeForcedToWait = totalTimeUIWaitedOnNetworkCalls / numberOfTimesUIWaitedOnNetworkCalls; - LogHelper.printDebug(() -> "UI thread forced to wait: " + numberOfTimesUIWaitedOnNetworkCalls + " times, " - + "total wait time: " + totalTimeUIWaitedOnNetworkCalls + "ms, " - + "average wait time: " + averageTimeForcedToWait + "ms"); - } } class VerticallyCenteredImageSpan extends ImageSpan { diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index 6664861f..3175675e 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -77,7 +77,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title")); percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> { SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.saveValue(newValue); - + ReturnYouTubeDislike.clearCache(); updateUIState(); return true; }); @@ -88,7 +88,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title")); compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> { SettingsEnum.RYD_USE_COMPACT_LAYOUT.saveValue(newValue); - + ReturnYouTubeDislike.clearCache(); updateUIState(); return true; }); From 5ba4cbd4e097d863924731d363dd5bb8849e1394 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 13 Apr 2023 08:32:47 +0200 Subject: [PATCH 16/30] feat(youtube/general-ads): hide new type of ad --- .../app/revanced/integrations/patches/GeneralAdsPatch.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index 96c41907..d4a346ca 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -65,7 +65,8 @@ public final class GeneralAdsPatch extends Filter { "text_image_button_layout", "primetime_promo", "feature_grid_interstitial", - "product_details" + "product_details", + "brand_video_shelf" ); var movieAds = new BlockRule( SettingsEnum.ADREMOVER_MOVIE_REMOVAL, From 14223f40b5ca48f35bbecfd849dff20dfd309d92 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 14 Apr 2023 05:50:16 +0400 Subject: [PATCH 17/30] feat(youtube): user selectable default video speed and quality (#354) Co-authored-by: johnconner122 <107796137+johnconner122@users.noreply.github.com> Co-authored-by: oSumAtrIX --- .../quality/RememberVideoQualityPatch.java | 233 ++++++++++-------- .../playback/speed/CustomVideoSpeedPatch.java | 13 +- .../speed/RememberPlaybackSpeedPatch.java | 42 +++- .../integrations/settings/SettingsEnum.java | 60 ++++- .../settings/SharedPrefCategory.java | 74 ++++-- .../ReVancedSettingsFragment.java | 68 +++-- 6 files changed, 321 insertions(+), 169 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java index 695b52ac..df85317d 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/quality/RememberVideoQualityPatch.java @@ -1,139 +1,168 @@ package app.revanced.integrations.patches.playback.quality; -import android.content.Context; +import static app.revanced.integrations.utils.ReVancedUtils.NetworkType; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collections; +import java.util.List; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; -import app.revanced.integrations.utils.ReVancedUtils.NetworkType; public class RememberVideoQualityPatch { - public static int selectedQuality1 = -2; - private static Boolean newVideo = false; - private static Boolean userChangedQuality = false; + private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2; + private static final SettingsEnum wifiQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_WIFI; + private static final SettingsEnum mobileQualitySetting = SettingsEnum.VIDEO_QUALITY_DEFAULT_MOBILE; - public static void changeDefaultQuality(int defaultQuality) { - Context context = ReVancedUtils.getContext(); + private static boolean qualityNeedsUpdating; + @Nullable + private static String currentVideoId; - var networkType = ReVancedUtils.getNetworkType(); + /** + * If the user selected a new quality from the flyout menu, + * and {@link SettingsEnum#VIDEO_QUALITY_REMEMBER_LAST_SELECTED} is enabled. + */ + private static boolean userChangedDefaultQuality; + /** + * Index of the video quality chosen by the user from the flyout menu. + */ + private static int userSelectedQualityIndex; + + /** + * The available qualities of the current video in human readable form: [1080, 720, 480] + */ + @Nullable + private static List videoQualities; + + private static void changeDefaultQuality(int defaultQuality) { + NetworkType networkType = ReVancedUtils.getNetworkType(); if (networkType == NetworkType.NONE) { - ReVancedUtils.showToastShort("No internet connection."); - } else { - var preferenceKey = "wifi_quality"; - var networkTypeMessage = "WIFI"; - - if (networkType == NetworkType.MOBILE) { - networkTypeMessage = "mobile"; - preferenceKey = "mobile_quality"; - } - - SharedPrefCategory.REVANCED_PREFS.saveString(preferenceKey, String.valueOf(defaultQuality)); - ReVancedUtils.showToastShort("Changing default " + networkTypeMessage + " quality to: " + defaultQuality); + ReVancedUtils.showToastShort("No internet connection"); + return; } - - userChangedQuality = false; + String networkTypeMessage; + if (networkType == NetworkType.MOBILE) { + mobileQualitySetting.saveValue(defaultQuality); + networkTypeMessage = "mobile"; + } else { + wifiQualitySetting.saveValue(defaultQuality); + networkTypeMessage = "Wi-Fi"; + } + ReVancedUtils.showToastShort("Changed default " + networkTypeMessage + + " quality to: " + defaultQuality +"p"); } - public static int setVideoQuality(Object[] qualities, int quality, Object qInterface, String qIndexMethod) { - Field[] fields; - - if (!(newVideo || userChangedQuality) || qInterface == null) { - return quality; - } - - Class intType = Integer.TYPE; - ArrayList iStreamQualities = new ArrayList<>(); + /** + * Injection point. + * + * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 + * @param originalQualityIndex quality index to use, as chosen by YouTube + */ + public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) { try { - for (Object streamQuality : qualities) { - for (Field field : streamQuality.getClass().getFields()) { - if (field.getType().isAssignableFrom(intType)) { // converts quality index to actual readable resolution - int value = field.getInt(streamQuality); - if (field.getName().length() <= 2) { - iStreamQualities.add(value); + if (!(qualityNeedsUpdating || userChangedDefaultQuality) || qInterface == null) { + return originalQualityIndex; + } + qualityNeedsUpdating = false; + + final int preferredQuality; + if (ReVancedUtils.getNetworkType() == NetworkType.MOBILE) { + preferredQuality = mobileQualitySetting.getInt(); + } else { + preferredQuality = wifiQualitySetting.getInt(); + } + if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) { + return originalQualityIndex; // nothing to do + } + + if (videoQualities == null || videoQualities.size() != qualities.length) { + videoQualities = new ArrayList<>(qualities.length); + for (Object streamQuality : qualities) { + for (Field field : streamQuality.getClass().getFields()) { + if (field.getType().isAssignableFrom(Integer.TYPE) + && field.getName().length() <= 2) { + videoQualities.add(field.getInt(streamQuality)); } } } + LogHelper.printDebug(() -> "VideoId: " + currentVideoId + " videoQualities: " + videoQualities); } - } catch (Exception ignored) { - } - Collections.sort(iStreamQualities); - int index = 0; - if (userChangedQuality) { - for (int convertedQuality : iStreamQualities) { - int selectedQuality2 = qualities.length - selectedQuality1 + 1; - index++; - if (selectedQuality2 == index) { - final int indexToLog = index; // must be final for lambda - LogHelper.printDebug(() -> "Quality index is: " + indexToLog + " and corresponding value is: " + convertedQuality); - changeDefaultQuality(convertedQuality); - return selectedQuality2; + + if (userChangedDefaultQuality) { + userChangedDefaultQuality = false; + final int quality = videoQualities.get(userSelectedQualityIndex); + LogHelper.printDebug(() -> "User changed default quality to: " + quality); + changeDefaultQuality(quality); + return userSelectedQualityIndex; + } + + // find the highest quality that is equal to or less than the preferred + int qualityToUse = videoQualities.get(0); // first element is automatic mode + int qualityIndexToUse = 0; + int i = 0; + for (Integer quality : videoQualities) { + if (quality <= preferredQuality && qualityToUse < quality) { + qualityToUse = quality; + qualityIndexToUse = i; } + i++; + } + if (qualityIndexToUse == originalQualityIndex) { + LogHelper.printDebug(() -> "Video is already preferred quality: " + preferredQuality); + return originalQualityIndex; } - } - newVideo = false; - final int qualityToLog = quality; - LogHelper.printDebug(() -> "Quality: " + qualityToLog); - Context context = ReVancedUtils.getContext(); - if (context == null) { - LogHelper.printException(() -> "Context is null or settings not initialized, returning quality: " + qualityToLog); - return quality; - } - var networkType = ReVancedUtils.getNetworkType(); - if (networkType == NetworkType.NONE) { - LogHelper.printDebug(() -> "No Internet connection!"); - return quality; - } else { - var preferenceKey = "wifi_quality"; - if (networkType == NetworkType.MOBILE) preferenceKey = "mobile_quality"; - int preferredQuality = SharedPrefCategory.REVANCED_PREFS.getInt(preferenceKey, -2); - if (preferredQuality == -2) return quality; + final int qualityToUseLog = qualityToUse; + LogHelper.printDebug(() -> "Quality changed from: " + + videoQualities.get(originalQualityIndex) + " to: " + qualityToUseLog); - for (int streamQuality2 : iStreamQualities) { - final int indexToLog = index; - LogHelper.printDebug(() -> "Quality at index " + indexToLog + ": " + streamQuality2); - index++; - } - for (Integer iStreamQuality : iStreamQualities) { - int streamQuality3 = iStreamQuality; - if (streamQuality3 <= preferredQuality) { - quality = streamQuality3; - } - } - if (quality == -2) return quality; - - int qualityIndex = iStreamQualities.indexOf(quality); - final int qualityToLog2 = quality; - LogHelper.printDebug(() -> "Index of quality " + qualityToLog2 + " is " + qualityIndex); - try { - Class cl = qInterface.getClass(); - Method m = cl.getMethod(qIndexMethod, Integer.TYPE); - LogHelper.printDebug(() -> "Method is: " + qIndexMethod); - m.invoke(qInterface, iStreamQualities.get(qualityIndex)); - LogHelper.printDebug(() -> "Quality changed to: " + qualityIndex); - return qualityIndex; - } catch (Exception ex) { - LogHelper.printException(() -> "Failed to set quality", ex); - return qualityIndex; - } + Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE); + m.invoke(qInterface, qualityToUse); + return qualityIndexToUse; + } catch (Exception ex) { + LogHelper.printException(() -> "Failed to set quality", ex); + return originalQualityIndex; } } + /** + * Injection point. + */ public static void userChangedQuality(int selectedQuality) { - if (!SettingsEnum.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.getBoolean()) return; + if (!SettingsEnum.VIDEO_QUALITY_REMEMBER_LAST_SELECTED.getBoolean()) return; - selectedQuality1 = selectedQuality; - userChangedQuality = true; + userSelectedQualityIndex = selectedQuality; + userChangedDefaultQuality = true; } - public static void newVideoStarted(String videoId) { - newVideo = true; + /** + * Injection point. + */ + public static void newVideoStarted(@NonNull String videoId) { + // The same videoId can be passed in multiple times for a single video playback. + // Such as closing and opening the app, and sometimes when turning off/on the device screen. + // + // Known limitation, if: + // 1. a default video quality exists, and remember quality is turned off + // 2. user opens a video + // 3. user changes the video quality + // 4. user turns off then on the device screen (or does anything else that triggers the video id hook) + // result: the video quality of the current video will revert back to the saved default + // + // qualityNeedsUpdating could be set only when the videoId changes + // but then if the user closes and re-opens the same video the default video quality will not be applied. + LogHelper.printDebug(() -> "newVideoStarted: " + videoId); + qualityNeedsUpdating = true; + + if (!videoId.equals(currentVideoId)) { + currentVideoId = videoId; + videoQualities = null; + } } } diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java index 663fe6b1..6692dc85 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/CustomVideoSpeedPatch.java @@ -1,9 +1,12 @@ package app.revanced.integrations.patches.playback.speed; public class CustomVideoSpeedPatch { - // Values are useless as they are being overridden by the respective patch. - // This generates a .array segment in Dalvik bytecode - // which the patch utilizes to store the video speeds in, only - // if it has two or more default values. - public static final float[] videoSpeeds = { 0, 0 }; + /** + * Default playback speeds offered by YouTube. + * Values are also used by {@link RememberPlaybackSpeedPatch}. + * + * If custom video speed is applied, + * then this array is overwritten by the patch with custom speeds + */ + public static final float[] videoSpeeds = {0.25f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f}; } diff --git a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java index 5b4a9460..b3475944 100644 --- a/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -1,5 +1,7 @@ package app.revanced.integrations.patches.playback.speed; +import android.preference.ListPreference; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -9,6 +11,11 @@ import app.revanced.integrations.utils.ReVancedUtils; public final class RememberPlaybackSpeedPatch { + /** + * PreferenceList entries and values, of all available playback speeds. + */ + private static String[] preferenceListEntries, preferenceListEntryValues; + @Nullable private static String currentVideoId; @@ -21,7 +28,7 @@ public final class RememberPlaybackSpeedPatch { return; } currentVideoId = videoId; - VideoInformation.overridePlaybackSpeed(SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat()); + VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat()); } /** @@ -31,13 +38,9 @@ public final class RememberPlaybackSpeedPatch { * @param playbackSpeed The playback speed the user selected */ public static void userSelectedPlaybackSpeed(float playbackSpeed) { - if (SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean()) { - SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.saveValue(playbackSpeed); - - // TODO: extract these strings into localized file - ReVancedUtils.showToastLong("Remembering playback speed: " + playbackSpeed + "x"); - } else if (playbackSpeed != (float) SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.defaultValue) { - ReVancedUtils.showToastLong("Applying playback speed: " + playbackSpeed + "x"); + if (SettingsEnum.PLAYBACK_SPEED_REMEMBER_LAST_SELECTED.getBoolean()) { + SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed); + ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x"); } } @@ -48,4 +51,27 @@ public final class RememberPlaybackSpeedPatch { public static float getPlaybackSpeedOverride() { return VideoInformation.getCurrentPlaybackSpeed(); } + + /** + * Initialize a settings preference list. + * + * Normally this is done during patching by creating a static xml preference list, + * but the available playback speeds differ depending if {@link CustomVideoSpeedPatch} is applied or not. + */ + public static void initializeListPreference(ListPreference preference) { + if (preferenceListEntries == null) { + float[] videoSpeeds = CustomVideoSpeedPatch.videoSpeeds; + preferenceListEntries = new String[videoSpeeds.length]; + preferenceListEntryValues = new String[videoSpeeds.length]; + int i = 0; + for (float speed : videoSpeeds) { + String speedString = String.valueOf(speed); + preferenceListEntries[i] = speedString + "x"; + preferenceListEntryValues[i] = speedString; + i++; + } + } + preference.setEntries(preferenceListEntries); + preference.setEntryValues(preferenceListEntryValues); + } } diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 8819bd6e..2ddad19d 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -29,9 +29,11 @@ public enum SettingsEnum { // Video settings OLD_STYLE_VIDEO_QUALITY_PLAYER_SETTINGS("revanced_use_old_style_quality_settings", BOOLEAN, TRUE), - REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), - REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE), - REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE("revanced_remember_playback_speed_last_selected_value", FLOAT, 1.0f), + VIDEO_QUALITY_REMEMBER_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE), + VIDEO_QUALITY_DEFAULT_WIFI("revanced_default_video_quality_wifi", INTEGER, -2), + VIDEO_QUALITY_DEFAULT_MOBILE("revanced_default_video_quality_mobile", INTEGER, -2), + PLAYBACK_SPEED_REMEMBER_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE), + PLAYBACK_SPEED_DEFAULT("revanced_default_playback_speed", FLOAT, 1.0f), // TODO: Unused currently // Whitelist settings @@ -295,13 +297,13 @@ public enum SettingsEnum { value = sharedPref.getBoolean(path, (boolean) defaultValue); break; case INTEGER: - value = sharedPref.getInt(path, (Integer) defaultValue); + value = sharedPref.getIntegerString(path, (Integer) defaultValue); break; case LONG: - value = sharedPref.getLong(path, (Long) defaultValue); + value = sharedPref.getLongString(path, (Long) defaultValue); break; case FLOAT: - value = sharedPref.getFloat(path, (Float) defaultValue); + value = sharedPref.getFloatString(path, (Float) defaultValue); break; case STRING: value = sharedPref.getString(path, (String) defaultValue); @@ -316,9 +318,37 @@ public enum SettingsEnum { * * This intentionally is a static method, to deter accidental usage * when {@link #saveValue(Object)} was intended. + * + * This method is only to be used by the Settings preference code. */ - public static void setValue(@NonNull SettingsEnum setting, @NonNull Object newValue) { - setting.value = Objects.requireNonNull(newValue); + public static void setValue(@NonNull SettingsEnum setting, @NonNull String newValue) { + Objects.requireNonNull(newValue); + switch (setting.returnType) { + case BOOLEAN: + setting.value = Boolean.valueOf(newValue); + break; + case INTEGER: + setting.value = Integer.valueOf(newValue); + break; + case LONG: + setting.value = Long.valueOf(newValue); + break; + case FLOAT: + setting.value = Float.valueOf(newValue); + break; + case STRING: + setting.value = newValue; + break; + default: + throw new IllegalStateException(setting.name()); + } + } + /** + * This method is only to be used by the Settings preference code. + */ + public static void setValue(@NonNull SettingsEnum setting, @NonNull Boolean newValue) { + Objects.requireNonNull(newValue); + setting.value = newValue; } /** @@ -331,13 +361,13 @@ public enum SettingsEnum { sharedPref.saveBoolean(path, (boolean) newValue); break; case INTEGER: - sharedPref.saveInt(path, (int) newValue); + sharedPref.saveIntegerString(path, (Integer) newValue); break; case LONG: - sharedPref.saveLong(path, (long) newValue); + sharedPref.saveLongString(path, (Long) newValue); break; case FLOAT: - sharedPref.saveFloat(path, (float) newValue); + sharedPref.saveFloatString(path, (Float) newValue); break; case STRING: sharedPref.saveString(path, (String) newValue); @@ -384,6 +414,14 @@ public enum SettingsEnum { return (String) value; } + /** + * @return the value of this setting as as generic object type. + */ + @NonNull + public Object getObjectValue() { + return value; + } + public enum ReturnType { BOOLEAN, INTEGER, diff --git a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java index 59413da3..4bf180ed 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java +++ b/app/src/main/java/app/revanced/integrations/settings/SharedPrefCategory.java @@ -4,11 +4,21 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.Objects; import app.revanced.integrations.utils.ReVancedUtils; +/** + * Shared categories, and helper methods. + * + * The various save methods store numbers as Strings, + * which is required if using {@link android.preference.PreferenceFragment}. + * + * If saved numbers will not be used with a preference fragment, + * then store the primitive numbers using {@link #preferences}. + */ public enum SharedPrefCategory { YOUTUBE("youtube"), RETURN_YOUTUBE_DISLIKE("ryd"), @@ -25,27 +35,41 @@ public enum SharedPrefCategory { preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE); } - public void saveString(@NonNull String key, @NonNull String value) { - Objects.requireNonNull(value); - preferences.edit().putString(key, value).apply(); + private void saveObjectAsString(@NonNull String key, @Nullable Object value) { + preferences.edit().putString(key, (value == null ? null : value.toString())).apply(); } public void saveBoolean(@NonNull String key, boolean value) { preferences.edit().putBoolean(key, value).apply(); } - public void saveInt(@NonNull String key, int value) { - preferences.edit().putInt(key, value).apply(); + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveIntegerString(@NonNull String key, @Nullable Integer value) { + saveObjectAsString(key, value); } - public void saveLong(@NonNull String key, long value) { - preferences.edit().putLong(key, value).apply(); + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveLongString(@NonNull String key, @Nullable Long value) { + saveObjectAsString(key, value); } - public void saveFloat(@NonNull String key, float value) { - preferences.edit().putFloat(key, value).apply(); + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveFloatString(@NonNull String key, @Nullable Float value) { + saveObjectAsString(key, value); } + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveString(@NonNull String key, @Nullable String value) { + saveObjectAsString(key, value); + } @NonNull public String getString(@NonNull String key, @NonNull String _default) { @@ -53,42 +77,50 @@ public enum SharedPrefCategory { return preferences.getString(key, _default); } + public boolean getBoolean(@NonNull String key, boolean _default) { return preferences.getBoolean(key, _default); } - // region Hack, required for PreferencesFragments to function correctly. unknown why required - @NonNull - public Integer getInt(@NonNull String key, @NonNull Integer _default) { + public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) { try { - return Integer.valueOf(preferences.getString(key, _default.toString())); + String value = preferences.getString(key, null); + if (value != null) { + return Integer.valueOf(value); + } + return _default; } catch (ClassCastException ex) { - return preferences.getInt(key, _default); + return preferences.getInt(key, _default); // old data, previously stored as primitive } } @NonNull - public Long getLong(@NonNull String key, @NonNull Long _default) { + public Long getLongString(@NonNull String key, @NonNull Long _default) { try { - return Long.valueOf(preferences.getString(key, _default.toString())); + String value = preferences.getString(key, null); + if (value != null) { + return Long.valueOf(value); + } + return _default; } catch (ClassCastException ex) { return preferences.getLong(key, _default); } } @NonNull - public Float getFloat(@NonNull String key, @NonNull Float _default) { + public Float getFloatString(@NonNull String key, @NonNull Float _default) { try { - return Float.valueOf(preferences.getString(key, _default.toString())); + String value = preferences.getString(key, null); + if (value != null) { + return Float.valueOf(value); + } + return _default; } catch (ClassCastException ex) { return preferences.getFloat(key, _default); } } - // endregion - - @NonNull @Override public String toString() { diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java index 8862f88e..16b427d2 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReVancedSettingsFragment.java @@ -13,6 +13,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.Process; import android.preference.EditTextPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; @@ -23,12 +24,18 @@ import androidx.annotation.Nullable; import com.google.android.apps.youtube.app.application.Shell_HomeActivity; +import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public class ReVancedSettingsFragment extends PreferenceFragment { + /** + * Used to prevent showing reboot dialog, if user cancels a setting user dialog. + */ + private boolean showingUserDialogMessage; + SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { SettingsEnum setting = SettingsEnum.settingFromPath(str); @@ -43,32 +50,22 @@ public class ReVancedSettingsFragment extends PreferenceFragment { SettingsEnum.setValue(setting, switchPref.isChecked()); } else if (pref instanceof EditTextPreference) { String editText = ((EditTextPreference) pref).getText(); - Object value; - switch (setting.returnType) { - case INTEGER: - value = Integer.parseInt(editText); - break; - case LONG: - value = Long.parseLong(editText); - break; - case FLOAT: - value = Float.parseFloat(editText); - break; - case STRING: - value = editText; - break; - default: - throw new IllegalStateException(setting.toString()); - } - SettingsEnum.setValue(setting, value); + SettingsEnum.setValue(setting, editText); + } else if (pref instanceof ListPreference) { + ListPreference listPref = (ListPreference) pref; + SettingsEnum.setValue(setting, listPref.getValue()); + updateListPreferenceSummary((ListPreference) pref, setting); } else { LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref); + return; } - if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { - showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting); - } else if (setting.rebootApp) { - rebootDialog(getActivity()); + if (!showingUserDialogMessage) { + if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { + showSettingUserDialogConfirmation(getActivity(), (SwitchPreference) pref, setting); + } else if (setting.rebootApp) { + rebootDialog(getActivity()); + } } enableDisablePreferences(); @@ -88,6 +85,20 @@ public class ReVancedSettingsFragment extends PreferenceFragment { enableDisablePreferences(); + // if the preference was included, then initialize it based on the available playback speed + Preference defaultSpeedPreference = findPreference(SettingsEnum.PLAYBACK_SPEED_DEFAULT.path); + if (defaultSpeedPreference instanceof ListPreference) { + RememberPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference); + } + + // set the summary text for any ListPreferences + for (SettingsEnum setting : SettingsEnum.values()) { + Preference preference = findPreference(setting.path); + if (preference instanceof ListPreference) { + updateListPreferenceSummary((ListPreference) preference, setting); + } + } + preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); } catch (Exception ex) { LogHelper.printException(() -> "onActivityCreated() error", ex); @@ -109,6 +120,13 @@ public class ReVancedSettingsFragment extends PreferenceFragment { } } + private void updateListPreferenceSummary(ListPreference listPreference, SettingsEnum setting) { + final int entryIndex = listPreference.findIndexOfValue(setting.getObjectValue().toString()); + if (entryIndex >= 0) { + listPreference.setSummary(listPreference.getEntries()[entryIndex]); + } + } + private void reboot(@NonNull Activity activity) { final int intentFlags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; PendingIntent intent = PendingIntent.getActivity(activity, 0, @@ -126,10 +144,12 @@ public class ReVancedSettingsFragment extends PreferenceFragment { reboot(activity); }) .setNegativeButton(negativeButton, null) + .setCancelable(false) .show(); } private void showSettingUserDialogConfirmation(@NonNull Activity activity, SwitchPreference switchPref, SettingsEnum setting) { + showingUserDialogMessage = true; new AlertDialog.Builder(activity) .setTitle(str("revanced_settings_confirm_user_dialog_title")) .setMessage(setting.userDialogMessage.toString()) @@ -143,6 +163,10 @@ public class ReVancedSettingsFragment extends PreferenceFragment { SettingsEnum.setValue(setting, defaultBooleanValue); switchPref.setChecked(defaultBooleanValue); }) + .setOnDismissListener(dialog -> { + showingUserDialogMessage = false; + }) + .setCancelable(false) .show(); } } From 76a01d1b7c270e07d3b76578dd38513db9354a3d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 16 Apr 2023 17:32:01 +0000 Subject: [PATCH 18/30] chore(release): 0.103.0-dev.1 [skip ci] # [0.103.0-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.102.0...v0.103.0-dev.1) (2023-04-16) ### Bug Fixes * **youtube/general-ads:** hide new types of ads ([#339](https://github.com/revanced/revanced-integrations/issues/339)) ([6528d44](https://github.com/revanced/revanced-integrations/commit/6528d444b49759ee1137c9b0eb8e1079fb4cc97c)) * **youtube/return-youtube-dislike:** fix error toast when voting ([#349](https://github.com/revanced/revanced-integrations/issues/349)) ([e3d923d](https://github.com/revanced/revanced-integrations/commit/e3d923d564ae572c5e0e10a86ce17b8009ec8c42)) * **youtube/return-youtube-dislike:** stale dislike data shown after opening / closing the app during shorts playback ([#356](https://github.com/revanced/revanced-integrations/issues/356)) ([212e4f2](https://github.com/revanced/revanced-integrations/commit/212e4f2ce43360776fe20467c4142c9936b22d42)) * **youtube/settings:** fix dialog not shown if dismissed with back button ([584de16](https://github.com/revanced/revanced-integrations/commit/584de16236ff758c2067ee84ba4cc04d765d49ba)) * **youtube/sponsorblock:** change default behavior to better match the browser ([#353](https://github.com/revanced/revanced-integrations/issues/353)) ([b959c8e](https://github.com/revanced/revanced-integrations/commit/b959c8ef98e869201c8bc2609943108283fea453)) * **youtube/sponsorblock:** settings do not show default behavior ([#351](https://github.com/revanced/revanced-integrations/issues/351)) ([6dbccfd](https://github.com/revanced/revanced-integrations/commit/6dbccfd472d843b5c3f0efed39b575d3ea7ac04f)) * **youtube/sponsorblock:** update HTTP user agent ([#344](https://github.com/revanced/revanced-integrations/issues/344)) ([3025103](https://github.com/revanced/revanced-integrations/commit/3025103014a4521a437cfde0a417535e7751b517)) ### Features * **youtube/general-ads:** block new type of ad ([6265a91](https://github.com/revanced/revanced-integrations/commit/6265a91841f8b037be22dd5fd5399dbeeb666745)) * **youtube/general-ads:** hide new type of ad ([5ba4cbd](https://github.com/revanced/revanced-integrations/commit/5ba4cbd4e097d863924731d363dd5bb8849e1394)) * **youtube/general-ads:** hide new type of ad ([f818490](https://github.com/revanced/revanced-integrations/commit/f8184905bd9601bf63e30963ff337d24f2599794)) * **youtube/settings:** disable preference control if the feature is turned off. show a dialog explaining side effects of some patches ([#328](https://github.com/revanced/revanced-integrations/issues/328)) ([a0ad968](https://github.com/revanced/revanced-integrations/commit/a0ad968aaa66422e67de2a61d76bc7aa88f08bf6)) * **youtube:** user selectable default video speed and quality ([#354](https://github.com/revanced/revanced-integrations/issues/354)) ([14223f4](https://github.com/revanced/revanced-integrations/commit/14223f40b5ca48f35bbecfd849dff20dfd309d92)) --- CHANGELOG.md | 22 ++++++++++++++++++++++ gradle.properties | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6da6fc4..d52db48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# [0.103.0-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.102.0...v0.103.0-dev.1) (2023-04-16) + + +### Bug Fixes + +* **youtube/general-ads:** hide new types of ads ([#339](https://github.com/revanced/revanced-integrations/issues/339)) ([6528d44](https://github.com/revanced/revanced-integrations/commit/6528d444b49759ee1137c9b0eb8e1079fb4cc97c)) +* **youtube/return-youtube-dislike:** fix error toast when voting ([#349](https://github.com/revanced/revanced-integrations/issues/349)) ([e3d923d](https://github.com/revanced/revanced-integrations/commit/e3d923d564ae572c5e0e10a86ce17b8009ec8c42)) +* **youtube/return-youtube-dislike:** stale dislike data shown after opening / closing the app during shorts playback ([#356](https://github.com/revanced/revanced-integrations/issues/356)) ([212e4f2](https://github.com/revanced/revanced-integrations/commit/212e4f2ce43360776fe20467c4142c9936b22d42)) +* **youtube/settings:** fix dialog not shown if dismissed with back button ([584de16](https://github.com/revanced/revanced-integrations/commit/584de16236ff758c2067ee84ba4cc04d765d49ba)) +* **youtube/sponsorblock:** change default behavior to better match the browser ([#353](https://github.com/revanced/revanced-integrations/issues/353)) ([b959c8e](https://github.com/revanced/revanced-integrations/commit/b959c8ef98e869201c8bc2609943108283fea453)) +* **youtube/sponsorblock:** settings do not show default behavior ([#351](https://github.com/revanced/revanced-integrations/issues/351)) ([6dbccfd](https://github.com/revanced/revanced-integrations/commit/6dbccfd472d843b5c3f0efed39b575d3ea7ac04f)) +* **youtube/sponsorblock:** update HTTP user agent ([#344](https://github.com/revanced/revanced-integrations/issues/344)) ([3025103](https://github.com/revanced/revanced-integrations/commit/3025103014a4521a437cfde0a417535e7751b517)) + + +### Features + +* **youtube/general-ads:** block new type of ad ([6265a91](https://github.com/revanced/revanced-integrations/commit/6265a91841f8b037be22dd5fd5399dbeeb666745)) +* **youtube/general-ads:** hide new type of ad ([5ba4cbd](https://github.com/revanced/revanced-integrations/commit/5ba4cbd4e097d863924731d363dd5bb8849e1394)) +* **youtube/general-ads:** hide new type of ad ([f818490](https://github.com/revanced/revanced-integrations/commit/f8184905bd9601bf63e30963ff337d24f2599794)) +* **youtube/settings:** disable preference control if the feature is turned off. show a dialog explaining side effects of some patches ([#328](https://github.com/revanced/revanced-integrations/issues/328)) ([a0ad968](https://github.com/revanced/revanced-integrations/commit/a0ad968aaa66422e67de2a61d76bc7aa88f08bf6)) +* **youtube:** user selectable default video speed and quality ([#354](https://github.com/revanced/revanced-integrations/issues/354)) ([14223f4](https://github.com/revanced/revanced-integrations/commit/14223f40b5ca48f35bbecfd849dff20dfd309d92)) + # [0.102.0](https://github.com/revanced/revanced-integrations/compare/v0.101.1...v0.102.0) (2023-04-13) diff --git a/gradle.properties b/gradle.properties index 02852e46..81307db1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.102.0 +version = 0.103.0-dev.1 From 03f09cf7bce1747b1d402f3a3e16dd69c364dfce Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 16 Apr 2023 22:18:32 +0400 Subject: [PATCH 19/30] feat(youtube/sponsorblock): skip to video highlight (#352) --- .../patches/PlayerTypeHookPatch.java | 2 +- .../SponsorBlockSettingsFragment.java | 8 +- .../integrations/shared/PlayerType.kt | 2 +- .../SegmentPlaybackController.java | 181 ++++++++++---- .../sponsorblock/SponsorBlockSettings.java | 35 +-- .../sponsorblock/SponsorBlockUtils.java | 60 +++-- .../objects/CategoryBehaviour.java | 86 ++++--- .../sponsorblock/objects/SegmentCategory.java | 67 ++++-- .../SegmentCategoryListPreference.java | 9 +- .../sponsorblock/objects/SponsorSegment.java | 29 +-- .../sponsorblock/requests/SBRequester.java | 16 +- .../sponsorblock/ui/SkipSponsorButton.java | 18 +- .../ui/SponsorBlockViewController.java | 222 +++++++++--------- 13 files changed, 439 insertions(+), 296 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java b/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java index 3e31d5ed..2f46f008 100644 --- a/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/PlayerTypeHookPatch.java @@ -28,7 +28,7 @@ public class PlayerTypeHookPatch { LogHelper.printException(() -> "Unknown PlayerType encountered: " + type); } else { PlayerType.setCurrent(newType); - LogHelper.printDebug(() -> "YouTubePlayerOverlaysLayout player type was updated to " + newType); + LogHelper.printDebug(() -> "PlayerType was updated to: " + newType); } } } diff --git a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java index 05da518b..61bd92c0 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/SponsorBlockSettingsFragment.java @@ -64,8 +64,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { try { final boolean enabled = SettingsEnum.SB_ENABLED.getBoolean(); if (!enabled) { - SponsorBlockViewController.hideSkipButton(); - SponsorBlockViewController.hideNewSegmentLayout(); + SponsorBlockViewController.hideAll(); SegmentPlaybackController.setCurrentVideoId(null); } else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()) { SponsorBlockViewController.hideNewSegmentLayout(); @@ -141,12 +140,13 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { addNewSegment.setOnPreferenceChangeListener((preference1, o) -> { Boolean newValue = (Boolean) o; if (newValue && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) { - SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true); new AlertDialog.Builder(preference1.getContext()) .setTitle(str("sb_guidelines_popup_title")) .setMessage(str("sb_guidelines_popup_content")) .setNegativeButton(str("sb_guidelines_popup_already_read"), null) .setPositiveButton(str("sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines()) + .setOnDismissListener(dialog -> SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true)) + .setCancelable(false) .show(); } SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue); @@ -350,7 +350,7 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment { segmentCategory.removeAll(); Activity activity = getActivity(); - for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category)); } } catch (Exception ex) { diff --git a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt index 371767c5..2cf2c4a9 100644 --- a/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt +++ b/app/src/main/java/app/revanced/integrations/shared/PlayerType.kt @@ -16,7 +16,7 @@ enum class PlayerType { WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED, WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED, - INLINE_MINIMAL, + INLINE_MINIMAL, // home feed video playback VIRTUAL_REALITY_FULLSCREEN, WATCH_WHILE_PICTURE_IN_PICTURE; diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java index d2cea53d..9b5e27c0 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java @@ -5,6 +5,7 @@ import static app.revanced.integrations.utils.StringRef.str; import android.graphics.Canvas; import android.graphics.Rect; import android.text.TextUtils; +import android.util.TypedValue; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -30,13 +31,39 @@ import app.revanced.integrations.utils.ReVancedUtils; * Class is not thread safe. All methods must be called on the main thread unless otherwise specified. */ public class SegmentPlaybackController { + /** + * Length of time to show a highlight segment manual skip. + * Because there is no scheduled hide of a skip to highlight, + * effectively this time value is rounded up to the next second. + */ + private static final long HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT = 3800; + + /* + * Highlight segments have zero length, as they are a point in time. + * Draw them on screen using a fixed width bar. + * Value is independent of device dpi. + */ + private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = 7; + @Nullable private static String currentVideoId; @Nullable private static SponsorSegment[] segmentsOfCurrentVideo; /** - * Current segment that user can manually skip + * Highlight segment, if one exists. + */ + @Nullable + private static SponsorSegment highlightSegment; + + /** + * Because loading can take time, show the skip to highlight for a few seconds after the segments load. + * This is the end time (in milliseconds) to no longer show the initial display skip to highlight. + */ + private static long highlightSegmentInitialShowEndTime; + + /** + * Current (non-highlight) segment that user can manually skip */ @Nullable private static SponsorSegment segmentCurrentlyPlaying; @@ -68,23 +95,28 @@ public class SegmentPlaybackController { Arrays.sort(segments); segmentsOfCurrentVideo = segments; calculateTimeWithoutSegments(); + + for (SponsorSegment segment : segments) { + if (segment.category == SegmentCategory.HIGHLIGHT) { + highlightSegment = segment; + return; + } + } + highlightSegment = null; } public static boolean currentVideoHasSegments() { return segmentsOfCurrentVideo != null && segmentsOfCurrentVideo.length > 0; } - @Nullable - static String getCurrentVideoId() { - return currentVideoId; - } - /** * Clears all downloaded data */ private static void clearData() { currentVideoId = null; segmentsOfCurrentVideo = null; + highlightSegment = null; + highlightSegmentInitialShowEndTime = 0; timeWithoutSegments = null; segmentCurrentlyPlaying = null; scheduledUpcomingSegment = null; // prevent any existing scheduled skip from running @@ -102,8 +134,7 @@ public class SegmentPlaybackController { ReVancedUtils.verifyOnMainThread(); SponsorBlockSettings.initialize(); clearData(); - SponsorBlockViewController.hideSkipButton(); - SponsorBlockViewController.hideNewSegmentLayout(); + SponsorBlockViewController.hideAll(); SponsorBlockUtils.clearUnsubmittedSegmentTimes(); LogHelper.printDebug(() -> "Initialized SponsorBlock"); } catch (Exception ex) { @@ -164,7 +195,19 @@ public class SegmentPlaybackController { return; } setSegmentsOfCurrentVideo(segments); - setVideoTime(VideoInformation.getVideoTime()); // check for any skips now, instead of waiting for the next update + + final long videoTime = VideoInformation.getVideoTime(); + // if the current video time is before the highlight + if (highlightSegment != null && videoTime < highlightSegment.end) { + if (highlightSegment.shouldAutoSkip()) { + skipSegment(highlightSegment, false); + return; + } + highlightSegmentInitialShowEndTime = System.currentTimeMillis() + + HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT; + } + // check for any skips now, instead of waiting for the next update to setVideoTime() + setVideoTime(videoTime); }); } catch (Exception ex) { LogHelper.printException(() -> "executeDownloadSegments failure", ex); @@ -196,7 +239,8 @@ public class SegmentPlaybackController { for (final SponsorSegment segment : segmentsOfCurrentVideo) { if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR - || segment.category.behaviour == CategoryBehaviour.IGNORE) { + || segment.category.behaviour == CategoryBehaviour.IGNORE + || segment.category == SegmentCategory.HIGHLIGHT) { continue; } if (segment.end <= millis) { @@ -217,7 +261,7 @@ public class SegmentPlaybackController { // Also prevents showing the skip button if user seeks into the last half second of the segment. final long minMillisOfSegmentRemainingThreshold = 500; if (segmentCurrentlyPlaying == segment - || !segment.timeIsNearEnd(millis, minMillisOfSegmentRemainingThreshold)) { + || !segment.endIsNear(millis, minMillisOfSegmentRemainingThreshold)) { foundCurrentSegment = segment; } else { LogHelper.printDebug(() -> "Ignoring segment that ends very soon: " + segment); @@ -248,7 +292,7 @@ public class SegmentPlaybackController { // This check is needed to prevent scheduled hide and show from clashing with each other. final long minTimeBetweenStartEndOfSegments = 1000; if (foundCurrentSegment == null - || !foundCurrentSegment.timeIsNearEnd(segment.start, minTimeBetweenStartEndOfSegments)) { + || !foundCurrentSegment.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) { foundUpcomingSegment = segment; } else { LogHelper.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment); @@ -256,16 +300,22 @@ public class SegmentPlaybackController { } } + if (highlightSegment != null && (millis < HIGHLIGHT_SEGMENT_DURATION_TO_SHOW_SKIP_PROMPT + || System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) { + SponsorBlockViewController.showSkipHighlightButton(highlightSegment); + } else { + SponsorBlockViewController.hideSkipHighlightButton(); + } if (segmentCurrentlyPlaying != foundCurrentSegment) { if (foundCurrentSegment == null) { LogHelper.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying); segmentCurrentlyPlaying = null; - SponsorBlockViewController.hideSkipButton(); + SponsorBlockViewController.hideSkipSegmentButton(); } else { segmentCurrentlyPlaying = foundCurrentSegment; LogHelper.printDebug(() -> "Showing segment: " + segmentCurrentlyPlaying); - SponsorBlockViewController.showSkipButton(foundCurrentSegment); + SponsorBlockViewController.showSkipSegmentButton(foundCurrentSegment); } } @@ -274,7 +324,7 @@ public class SegmentPlaybackController { // schedule a hide, only if the segment end is near final SponsorSegment segmentToHide = - (foundCurrentSegment != null && foundCurrentSegment.timeIsNearEnd(millis, lookAheadMilliseconds)) + (foundCurrentSegment != null && foundCurrentSegment.endIsNear(millis, lookAheadMilliseconds)) ? foundCurrentSegment : null; @@ -294,7 +344,7 @@ public class SegmentPlaybackController { scheduledHideSegment = null; final long videoTime = VideoInformation.getVideoTime(); - if (!segmentToHide.timeIsNearEnd(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) { + if (!segmentToHide.endIsNear(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) { // current video time is not what's expected. User paused playback LogHelper.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide + " videoInformation time: " + videoTime); @@ -306,7 +356,7 @@ public class SegmentPlaybackController { // Should not use VideoInformation time as it is less accurate, // but this scheduled handler was scheduled precisely so we can just use the segment end time segmentCurrentlyPlaying = null; - SponsorBlockViewController.hideSkipButton(); + SponsorBlockViewController.hideSkipSegmentButton(); setVideoTime(segmentToHide.end); }, delayUntilHide); } @@ -330,7 +380,7 @@ public class SegmentPlaybackController { scheduledUpcomingSegment = null; final long videoTime = VideoInformation.getVideoTime(); - if (!segmentToSkip.timeIsNearStart(videoTime, + if (!segmentToSkip.startIsNear(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) { // current video time is not what's expected. User paused playback LogHelper.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip @@ -343,7 +393,7 @@ public class SegmentPlaybackController { } else { LogHelper.printDebug(() -> "Running scheduled show segment: " + segmentToSkip); segmentCurrentlyPlaying = segmentToSkip; - SponsorBlockViewController.showSkipButton(segmentToSkip); + SponsorBlockViewController.showSkipSegmentButton(segmentToSkip); } }, delayUntilSkip); } @@ -357,7 +407,7 @@ public class SegmentPlaybackController { private static SponsorSegment lastSegmentSkipped; private static long lastSegmentSkippedTime; - private static void skipSegment(@NonNull SponsorSegment segment, boolean userManuallySkipped) { + private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) { try { // If trying to seek to end of the video, YouTube can seek just short of the actual end. // (especially if the video does not end on a whole second boundary). @@ -365,23 +415,27 @@ public class SegmentPlaybackController { // Check for and ignore repeated skip attempts of the same segment over a short time period. final long now = System.currentTimeMillis(); final long minimumMillisecondsBetweenSkippingSameSegment = 500; - if ((lastSegmentSkipped == segment) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) { - LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segment); + if ((lastSegmentSkipped == segmentToSkip) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) { + LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segmentToSkip); return; } - LogHelper.printDebug(() -> "Skipping segment: " + segment); - lastSegmentSkipped = segment; + LogHelper.printDebug(() -> "Skipping segment: " + segmentToSkip); + lastSegmentSkipped = segmentToSkip; lastSegmentSkippedTime = now; segmentCurrentlyPlaying = null; - scheduledHideSegment = null; // if a scheduled has not run yet + scheduledHideSegment = null; scheduledUpcomingSegment = null; - SponsorBlockViewController.hideSkipButton(); + if (segmentToSkip == highlightSegment) { + highlightSegmentInitialShowEndTime = 0; + } + SponsorBlockViewController.hideSkipHighlightButton(); + SponsorBlockViewController.hideSkipSegmentButton(); - final boolean seekSuccessful = VideoInformation.seekTo(segment.end); + final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end); if (!seekSuccessful) { // can happen when switching videos and is normal - LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segment); + LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip); return; } @@ -389,10 +443,10 @@ public class SegmentPlaybackController { // check for any smaller embedded segments, and count those as autoskipped final boolean showSkipToast = SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean(); for (final SponsorSegment otherSegment : segmentsOfCurrentVideo) { - if (segment.end <= otherSegment.start) { + if (segmentToSkip.end < otherSegment.start) { break; // no other segments can be contained } - if (segment.containsSegment(otherSegment)) { // includes checking the segment against itself + if (segmentToSkip.containsSegment(otherSegment)) { // includes checking the segment against itself otherSegment.didAutoSkipped = true; // skipped this segment as well if (showSkipToast) { showSkippedSegmentToast(otherSegment); @@ -401,19 +455,19 @@ public class SegmentPlaybackController { } } - if (segment.category == SegmentCategory.UNSUBMITTED) { + if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) { // skipped segment was a preview of unsubmitted segment // remove the segment from the UI view SponsorBlockUtils.setNewSponsorSegmentPreviewed(); SponsorSegment[] newSegments = new SponsorSegment[segmentsOfCurrentVideo.length - 1]; int i = 0; - for (SponsorSegment sponsorSegment : segmentsOfCurrentVideo) { - if (sponsorSegment != segment) - newSegments[i++] = sponsorSegment; + for (SponsorSegment segment : segmentsOfCurrentVideo) { + if (segment != segmentToSkip) + newSegments[i++] = segment; } setSegmentsOfCurrentVideo(newSegments); } else { - SponsorBlockUtils.sendViewRequestAsync(segment); + SponsorBlockUtils.sendViewRequestAsync(segmentToSkip); } } catch (Exception ex) { LogHelper.printException(() -> "skipSegment failure", ex); @@ -433,7 +487,7 @@ public class SegmentPlaybackController { } toastSegmentSkipped = segment; - final long delayToToastMilliseconds = 200; // also the maximum time between skips to be considered skipping multiple segments + final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments ReVancedUtils.runOnMainThreadDelayed(() -> { try { if (toastSegmentSkipped == null) { // video was changed just after skipping segment @@ -452,12 +506,20 @@ public class SegmentPlaybackController { }, delayToToastMilliseconds); } - public static void onSkipSponsorClicked() { - if (segmentCurrentlyPlaying != null) { - skipSegment(segmentCurrentlyPlaying, true); - } else { - SponsorBlockViewController.hideSkipButton(); - LogHelper.printException(() -> "error: segment not available to skip"); // should never happen + /** + * @param segment can be either a highlight or a regular manual skip segment + */ + public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) { + try { + if (segment != highlightSegment && segment != segmentCurrentlyPlaying) { + LogHelper.printException(() -> "error: segment not available to skip"); // should never happen + SponsorBlockViewController.hideSkipSegmentButton(); + SponsorBlockViewController.hideSkipHighlightButton(); + return; + } + skipSegment(segment, true); + } catch (Exception ex) { + LogHelper.printException(() -> "onSkipSegmentClicked failure", ex); } } @@ -512,17 +574,17 @@ public class SegmentPlaybackController { * Injection point */ public static void setSponsorBarThickness(final int thickness) { - try { - setSponsorBarThickness((float) thickness); - } catch (Exception ex) { - LogHelper.printException(() -> "setSponsorBarThickness failure", ex); - } + setSponsorBarThickness((float) thickness); } public static void setSponsorBarThickness(final float thickness) { - if (sponsorBarThickness != thickness) { - LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness)); - sponsorBarThickness = thickness; + try { + if (sponsorBarThickness != thickness) { + LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness)); + sponsorBarThickness = thickness; + } + } catch (Exception ex) { + LogHelper.printException(() -> "setSponsorBarThickness failure", ex); } } @@ -564,6 +626,16 @@ public class SegmentPlaybackController { } } + private static int highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use + private static int getHighlightSegmentTimeBarScreenWidth() { + if (highlightSegmentTimeBarScreenWidth == -1) { + highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH, + ReVancedUtils.getContext().getResources().getDisplayMetrics()); + } + return highlightSegmentTimeBarScreenWidth; + } + /** * Injection point */ @@ -582,8 +654,13 @@ public class SegmentPlaybackController { final float tmp1 = (1f / currentVideoLength) * (absoluteRight - absoluteLeft); for (SponsorSegment segment : segmentsOfCurrentVideo) { - float left = segment.start * tmp1 + absoluteLeft; - float right = segment.end * tmp1 + absoluteLeft; + final float left = segment.start * tmp1 + absoluteLeft; + final float right; + if (segment.category == SegmentCategory.HIGHLIGHT) { + right = left + getHighlightSegmentTimeBarScreenWidth(); + } else { + right = segment.end * tmp1 + absoluteLeft; + } canvas.drawRect(left, top, right, bottom, segment.category.paint); } } catch (Exception ex) { diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java index f8f4fffa..5565ac5b 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockSettings.java @@ -29,11 +29,13 @@ public class SponsorBlockSettings { JSONObject barTypesObject = settingsJson.getJSONObject("barTypes"); JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections"); - for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { - // clear existing behavior, as browser plugin exports no value for ignored categories + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + // clear existing behavior, as browser plugin exports no behavior for ignored categories category.behaviour = CategoryBehaviour.IGNORE; - JSONObject categoryObject = barTypesObject.getJSONObject(category.key); - category.setColor(categoryObject.getString("color")); + if (barTypesObject.has(category.key)) { + JSONObject categoryObject = barTypesObject.getJSONObject(category.key); + category.setColor(categoryObject.getString("color")); + } } for (int i = 0; i < categorySelectionsArray.length(); i++) { @@ -47,16 +49,19 @@ public class SponsorBlockSettings { final int desktopKey = categorySelectionObject.getInt("option"); CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKey(desktopKey); - if (behaviour != null) { - category.behaviour = behaviour; + if (behaviour == null) { + ReVancedUtils.showToastLong(categoryKey + " unknown behavior key: " + desktopKey); + } else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) { + ReVancedUtils.showToastLong("Skip-once behavior not allowed for " + category.key); + category.behaviour = CategoryBehaviour.SKIP_AUTOMATICALLY; // use closest match } else { - LogHelper.printException(() -> "Unknown segment category behavior key: " + desktopKey); + category.behaviour = behaviour; } } SegmentCategory.updateEnabledCategories(); SharedPreferences.Editor editor = SharedPrefCategory.SPONSOR_BLOCK.preferences.edit(); - for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) { + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { category.save(editor); } editor.apply(); @@ -117,17 +122,19 @@ public class SponsorBlockSettings { JSONObject barTypesObject = new JSONObject(); // categories' colors JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior - SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted(); + SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted(); for (SegmentCategory category : categories) { JSONObject categoryObject = new JSONObject(); String categoryKey = category.key; categoryObject.put("color", category.colorString()); barTypesObject.put(categoryKey, categoryObject); - JSONObject behaviorObject = new JSONObject(); - behaviorObject.put("name", categoryKey); - behaviorObject.put("option", category.behaviour.desktopKey); - categorySelectionsArray.put(behaviorObject); + if (category.behaviour != CategoryBehaviour.IGNORE) { + JSONObject behaviorObject = new JSONObject(); + behaviorObject.put("name", categoryKey); + behaviorObject.put("option", category.behaviour.desktopKey); + categorySelectionsArray.put(behaviorObject); + } } json.put("userID", SettingsEnum.SB_UUID.getString()); json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean()); @@ -182,7 +189,7 @@ public class SponsorBlockSettings { initialized = true; String uuid = SettingsEnum.SB_UUID.getString(); - if (uuid == null || uuid.isEmpty()) { + if (uuid.isEmpty()) { uuid = (UUID.randomUUID().toString() + UUID.randomUUID().toString() + UUID.randomUUID().toString()) diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java index d5b0b6f6..6a19c0d3 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SponsorBlockUtils.java @@ -15,10 +15,8 @@ import java.lang.ref.WeakReference; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Duration; -import java.util.ArrayList; import java.util.Arrays; import java.util.Date; -import java.util.List; import java.util.Objects; import java.util.TimeZone; @@ -74,8 +72,8 @@ public class SponsorBlockUtils { @Override public void onClick(DialogInterface dialog, int which) { try { - SegmentCategory category = SegmentCategory.valuesWithoutUnsubmitted()[which]; - boolean enableButton; + SegmentCategory category = SegmentCategory.categoriesWithoutHighlights()[which]; + final boolean enableButton; if (category.behaviour == CategoryBehaviour.IGNORE) { ReVancedUtils.showToastLong(str("sb_new_segment_disabled_category")); enableButton = false; @@ -100,7 +98,7 @@ public class SponsorBlockUtils { Context context = ((AlertDialog) dialog).getContext(); dialog.dismiss(); - SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted(); + SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights(); CharSequence[] titles = new CharSequence[categories.length]; for (int i = 0, length = categories.length; i < length; i++) { titles[i] = categories[i].getTitleWithColorDot(); @@ -167,7 +165,9 @@ public class SponsorBlockUtils { } SponsorSegment segment = currentSegments[which]; - SegmentVote[] voteOptions = SegmentVote.values(); + SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT) + ? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category + : SegmentVote.values(); CharSequence[] items = new CharSequence[voteOptions.length]; for (int i = 0; i < voteOptions.length; i++) { @@ -195,7 +195,7 @@ public class SponsorBlockUtils { }) .show(); } catch (Exception ex) { - LogHelper.printException(() -> "onPreviewClicked failure", ex); + LogHelper.printException(() -> "segmentVoteClickListener failure", ex); } }; @@ -218,11 +218,12 @@ public class SponsorBlockUtils { final String uuid = SettingsEnum.SB_UUID.getString(); final long start = newSponsorSegmentStartMillis; final long end = newSponsorSegmentEndMillis; - final String videoId = SegmentPlaybackController.getCurrentVideoId(); + final String videoId = VideoInformation.getCurrentVideoId(); final long videoLength = VideoInformation.getCurrentVideoLength(); final SegmentCategory segmentCategory = newUserCreatedSegmentCategory; - if (start < 0 || end < 0 || start >= end || videoLength <= 0 || segmentCategory == null || videoId == null || uuid == null) { - LogHelper.printException(() -> "Unable to submit times, invalid parameters"); + if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty() + || segmentCategory == null || uuid.isEmpty()) { + LogHelper.printException(() -> "invalid parameters"); return; } clearUnsubmittedSegmentTimes(); @@ -258,9 +259,13 @@ public class SponsorBlockUtils { public static void onPublishClicked() { try { ReVancedUtils.verifyOnMainThread(); - if (!newSponsorSegmentPreviewed) { + if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) { + ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first")); + } else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) { + ReVancedUtils.showToastShort(str("sb_new_segment_start_is_before_end")); + } else if (!newSponsorSegmentPreviewed && newSponsorSegmentStartMillis != 0) { ReVancedUtils.showToastLong(str("sb_new_segment_preview_segment_first")); - } else if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { + } else { long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000; long start = (newSponsorSegmentStartMillis) / 1000; long end = (newSponsorSegmentEndMillis) / 1000; @@ -273,8 +278,6 @@ public class SponsorBlockUtils { .setNegativeButton(android.R.string.no, null) .setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener) .show(); - } else { - ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first")); } } catch (Exception ex) { LogHelper.printException(() -> "onPublishClicked failure", ex); @@ -303,29 +306,32 @@ public class SponsorBlockUtils { } else if (currentVideoLength < (10 * 60 * 60 * 1000)) { formatPattern = "H:mm:ss"; // less than 10 hours } else { - formatPattern = "HH:mm:ss"; // why is this on YouTube + formatPattern = "HH:mm:ss"; // why is this on YouTube } voteSegmentTimeFormatter.applyPattern(formatPattern); final int numberOfSegments = currentSegments.length; - List titles = new ArrayList<>(numberOfSegments); + CharSequence[] titles = new CharSequence[numberOfSegments]; for (int i = 0; i < numberOfSegments; i++) { SponsorSegment segment = currentSegments[i]; if (segment.category == SegmentCategory.UNSUBMITTED) { continue; } - String start = voteSegmentTimeFormatter.format(new Date(segment.start)); - String end = voteSegmentTimeFormatter.format(new Date(segment.end)); StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.append(String.format(" %s
%s to %s", - segment.category.color, segment.category.title, start, end)); + htmlBuilder.append(String.format(" %s
", + segment.category.color, segment.category.title)); + htmlBuilder.append(voteSegmentTimeFormatter.format(new Date(segment.start))); + if (segment.category != SegmentCategory.HIGHLIGHT) { + htmlBuilder.append(" to ").append(voteSegmentTimeFormatter.format(new Date(segment.end))); + } + htmlBuilder.append("
"); if (i + 1 != numberOfSegments) // prevents trailing new line after last segment htmlBuilder.append("
"); - titles.add(Html.fromHtml(htmlBuilder.toString())); + titles[i] = Html.fromHtml(htmlBuilder.toString()); } new AlertDialog.Builder(context) - .setItems(titles.toArray(new CharSequence[0]), segmentVoteClickListener) + .setItems(titles, segmentVoteClickListener) .show(); } catch (Exception ex) { LogHelper.printException(() -> "onVotingClicked failure", ex); @@ -335,7 +341,7 @@ public class SponsorBlockUtils { private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) { try { ReVancedUtils.verifyOnMainThread(); - final SegmentCategory[] values = SegmentCategory.valuesWithoutUnsubmitted(); + final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights(); CharSequence[] titles = new CharSequence[values.length]; for (int i = 0; i < values.length; i++) { titles[i] = values[i].getTitleWithColorDot(); @@ -353,7 +359,11 @@ public class SponsorBlockUtils { public static void onPreviewClicked() { try { ReVancedUtils.verifyOnMainThread(); - if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) { + if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) { + ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first")); + } else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) { + ReVancedUtils.showToastShort(str("sb_new_segment_start_is_before_end")); + } else { VideoInformation.seekTo(newSponsorSegmentStartMillis - 2500); final SponsorSegment[] original = SegmentPlaybackController.getSegmentsOfCurrentVideo(); final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1); @@ -362,8 +372,6 @@ public class SponsorBlockUtils { newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, false); SegmentPlaybackController.setSegmentsOfCurrentVideo(segments); - } else { - ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first")); } } catch (Exception ex) { LogHelper.printException(() -> "onPreviewClicked failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java index 0e226662..9b15d086 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/CategoryBehaviour.java @@ -11,32 +11,29 @@ import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.StringRef; public enum CategoryBehaviour { - SKIP_AUTOMATICALLY("skip", 2, sf("sb_skip_automatically"), true), + SKIP_AUTOMATICALLY("skip", 2, true, sf("sb_skip_automatically")), // desktop does not have skip-once behavior. Key is unique to ReVanced - SKIP_AUTOMATICALLY_ONCE("skip-once", 4, sf("sb_skip_automatically_once"), true), - MANUAL_SKIP("manual-skip", 1, sf("sb_skip_showbutton"), false), - SHOW_IN_SEEKBAR("seekbar-only", 0, sf("sb_skip_seekbaronly"), false), - // Ignore is the default behavior if no desktop behavior key is present - IGNORE("ignore", 3, sf("sb_skip_ignore"), false); + SKIP_AUTOMATICALLY_ONCE("skip-once", 3, true, sf("sb_skip_automatically_once")), + MANUAL_SKIP("manual-skip", 1, false, sf("sb_skip_showbutton")), + SHOW_IN_SEEKBAR("seekbar-only", 0, false, sf("sb_skip_seekbaronly")), + // ignored categories are not exported to json, and ignore is the default behavior when importing + IGNORE("ignore", -1, false, sf("sb_skip_ignore")); @NonNull public final String key; public final int desktopKey; - @NonNull - public final StringRef name; /** * If the segment should skip automatically */ - public final boolean skip; + public final boolean skipAutomatically; + @NonNull + public final StringRef description; - CategoryBehaviour(String key, - int desktopKey, - StringRef name, - boolean skip) { + CategoryBehaviour(String key, int desktopKey, boolean skipAutomatically, StringRef description) { this.key = Objects.requireNonNull(key); this.desktopKey = desktopKey; - this.name = Objects.requireNonNull(name); - this.skip = skip; + this.skipAutomatically = skipAutomatically; + this.description = Objects.requireNonNull(description); } @Nullable @@ -60,31 +57,60 @@ public enum CategoryBehaviour { } private static String[] behaviorKeys; - private static String[] behaviorNames; + private static String[] behaviorDescriptions; + + private static String[] behaviorKeysWithoutSkipOnce; + private static String[] behaviorDescriptionsWithoutSkipOnce; private static void createNameAndKeyArrays() { ReVancedUtils.verifyOnMainThread(); + CategoryBehaviour[] behaviours = values(); - behaviorKeys = new String[behaviours.length]; - behaviorNames = new String[behaviours.length]; - for (int i = 0, length = behaviours.length; i < length; i++) { - CategoryBehaviour behaviour = behaviours[i]; - behaviorKeys[i] = behaviour.key; - behaviorNames[i] = behaviour.name.toString(); + final int behaviorLength = behaviours.length; + behaviorKeys = new String[behaviorLength]; + behaviorDescriptions = new String[behaviorLength]; + behaviorKeysWithoutSkipOnce = new String[behaviorLength - 1]; + behaviorDescriptionsWithoutSkipOnce = new String[behaviorLength - 1]; + + int behaviorIndex = 0, behaviorHighlightIndex = 0; + while (behaviorIndex < behaviorLength) { + CategoryBehaviour behaviour = behaviours[behaviorIndex]; + String key = behaviour.key; + String description = behaviour.description.toString(); + behaviorKeys[behaviorIndex] = key; + behaviorDescriptions[behaviorIndex] = description; + behaviorIndex++; + if (behaviour != SKIP_AUTOMATICALLY_ONCE) { + behaviorKeysWithoutSkipOnce[behaviorHighlightIndex] = key; + behaviorDescriptionsWithoutSkipOnce[behaviorHighlightIndex] = description; + behaviorHighlightIndex++; + } } } - public static String[] getBehaviorNames() { - if (behaviorNames == null) { - createNameAndKeyArrays(); - } - return behaviorNames; - } - - public static String[] getBehaviorKeys() { + static String[] getBehaviorKeys() { if (behaviorKeys == null) { createNameAndKeyArrays(); } return behaviorKeys; } + static String[] getBehaviorKeysWithoutSkipOnce() { + if (behaviorKeysWithoutSkipOnce == null) { + createNameAndKeyArrays(); + } + return behaviorKeysWithoutSkipOnce; + } + + static String[] getBehaviorDescriptions() { + if (behaviorDescriptions == null) { + createNameAndKeyArrays(); + } + return behaviorDescriptions; + } + static String[] getBehaviorDescriptionsWithoutSkipOnce() { + if (behaviorDescriptionsWithoutSkipOnce == null) { + createNameAndKeyArrays(); + } + return behaviorDescriptionsWithoutSkipOnce; + } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java index f5f0da4f..2a6d3059 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategory.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.settings.SharedPrefCategory; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.StringRef; @@ -33,6 +34,11 @@ public enum SegmentCategory { MANUAL_SKIP, 0xFFFF00), INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"), MANUAL_SKIP, 0xCC00FF), + /** + * Unique category that is treated differently than the rest. + */ + HIGHLIGHT("poi_highlight", sf("sb_segments_highlight"), sf("sb_segments_highlight_sum"), sf("sb_skip_button_highlight"), sf("sb_skipped_highlight"), + MANUAL_SKIP, 0xFF1684), INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"), sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"), sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"), @@ -50,7 +56,10 @@ public enum SegmentCategory { UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("sb_skip_button_unsubmitted"), sf("sb_skipped_unsubmitted"), SKIP_AUTOMATICALLY, 0xFFFFFF); - private static final SegmentCategory[] mValuesWithoutUnsubmitted = new SegmentCategory[]{ + private static final StringRef skipSponsorTextCompact = sf("sb_skip_button_compact"); + private static final StringRef skipSponsorTextCompactHighlight = sf("sb_skip_button_compact_highlight"); + + private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{ SPONSOR, SELF_PROMO, INTERACTION, @@ -60,7 +69,19 @@ public enum SegmentCategory { FILLER, MUSIC_OFFTOPIC, }; - private static final Map mValuesMap = new HashMap<>(2 * mValuesWithoutUnsubmitted.length); + + private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{ + SPONSOR, + SELF_PROMO, + INTERACTION, + HIGHLIGHT, + INTRO, + OUTRO, + PREVIEW, + FILLER, + MUSIC_OFFTOPIC, + }; + private static final Map mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length); private static final String COLOR_PREFERENCE_KEY_SUFFIX = "_color"; @@ -70,13 +91,18 @@ public enum SegmentCategory { public static String sponsorBlockAPIFetchCategories = "[]"; static { - for (SegmentCategory value : mValuesWithoutUnsubmitted) + for (SegmentCategory value : categoriesWithoutUnsubmitted) mValuesMap.put(value.key, value); } @NonNull - public static SegmentCategory[] valuesWithoutUnsubmitted() { - return mValuesWithoutUnsubmitted; + public static SegmentCategory[] categoriesWithoutUnsubmitted() { + return categoriesWithoutUnsubmitted; + } + + @NonNull + public static SegmentCategory[] categoriesWithoutHighlights() { + return categoriesWithoutHighlights; } @Nullable @@ -87,7 +113,7 @@ public enum SegmentCategory { public static void loadFromPreferences() { SharedPreferences preferences = SharedPrefCategory.SPONSOR_BLOCK.preferences; LogHelper.printDebug(() -> "loadFromPreferences"); - for (SegmentCategory category : valuesWithoutUnsubmitted()) { + for (SegmentCategory category : categoriesWithoutUnsubmitted()) { category.load(preferences); } updateEnabledCategories(); @@ -97,7 +123,7 @@ public enum SegmentCategory { * Must be called if behavior of any category is changed */ public static void updateEnabledCategories() { - SegmentCategory[] categories = valuesWithoutUnsubmitted(); + SegmentCategory[] categories = categoriesWithoutUnsubmitted(); List enabledCategories = new ArrayList<>(categories.length); for (SegmentCategory category : categories) { if (category.behaviour != CategoryBehaviour.IGNORE) { @@ -157,6 +183,7 @@ public enum SegmentCategory { * If value is changed, then also call {@link #save(SharedPreferences.Editor)} */ public int color; + /** * If value is changed, then also call {@link #updateEnabledCategories()} */ @@ -272,17 +299,23 @@ public enum SegmentCategory { * @return the skip button text */ @NonNull - public String getSkipButtonText(long segmentStartTime, long videoLength) { + StringRef getSkipButtonText(long segmentStartTime, long videoLength) { + if (SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean()) { + return (this == SegmentCategory.HIGHLIGHT) + ? skipSponsorTextCompactHighlight + : skipSponsorTextCompact; + } + if (videoLength == 0) { - return skipButtonTextBeginning.toString(); // video is still loading. Assume it's the beginning + return skipButtonTextBeginning; // video is still loading. Assume it's the beginning } final float position = segmentStartTime / (float) videoLength; if (position < 0.25f) { - return skipButtonTextBeginning.toString(); + return skipButtonTextBeginning; } else if (position < 0.75f) { - return skipButtonTextMiddle.toString(); + return skipButtonTextMiddle; } - return skipButtonTextEnd.toString(); + return skipButtonTextEnd; } /** @@ -291,16 +324,16 @@ public enum SegmentCategory { * @return 'skipped segment' toast message */ @NonNull - public String getSkippedToastText(long segmentStartTime, long videoLength) { + StringRef getSkippedToastText(long segmentStartTime, long videoLength) { if (videoLength == 0) { - return skippedToastBeginning.toString(); // video is still loading. Assume it's the beginning + return skippedToastBeginning; // video is still loading. Assume it's the beginning } final float position = segmentStartTime / (float) videoLength; if (position < 0.25f) { - return skippedToastBeginning.toString(); + return skippedToastBeginning; } else if (position < 0.75f) { - return skippedToastMiddle.toString(); + return skippedToastMiddle; } - return skippedToastEnd.toString(); + return skippedToastEnd; } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java index b1ab4683..b2e9e2c9 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SegmentCategoryListPreference.java @@ -29,11 +29,16 @@ public class SegmentCategoryListPreference extends ListPreference { public SegmentCategoryListPreference(Context context, SegmentCategory category) { super(context); + final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT; this.category = Objects.requireNonNull(category); setKey(category.key); setDefaultValue(category.behaviour.key); - setEntries(CategoryBehaviour.getBehaviorNames()); - setEntryValues(CategoryBehaviour.getBehaviorKeys()); + setEntries(isHighlightCategory + ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce() + : CategoryBehaviour.getBehaviorDescriptions()); + setEntryValues(isHighlightCategory + ? CategoryBehaviour.getBehaviorKeysWithoutSkipOnce() + : CategoryBehaviour.getBehaviorKeys()); setSummary(category.description.toString()); updateTitle(); } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java index 98156969..dff6e230 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/objects/SponsorSegment.java @@ -14,6 +14,11 @@ public class SponsorSegment implements Comparable { DOWNVOTE(sf("sb_vote_downvote"), 0, true), CATEGORY_CHANGE(sf("sb_vote_category"), -1, true); // apiVoteType is not used for category change + public static final SegmentVote[] voteTypesWithoutCategoryChange = { + UPVOTE, + DOWNVOTE, + }; + @NonNull public final StringRef title; public final int apiVoteType; @@ -51,36 +56,28 @@ public class SponsorSegment implements Comparable { } public boolean shouldAutoSkip() { - return category.behaviour.skip && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE); + return category.behaviour.skipAutomatically && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE); } /** * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number */ - public boolean timeIsNearStart(long videoTime, long nearThreshold) { + public boolean startIsNear(long videoTime, long nearThreshold) { return Math.abs(start - videoTime) <= nearThreshold; } /** * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number */ - public boolean timeIsNearEnd(long videoTime, long nearThreshold) { + public boolean endIsNear(long videoTime, long nearThreshold) { return Math.abs(end - videoTime) <= nearThreshold; } /** - * @param nearThreshold threshold to declare the time parameter is near this segment - * @return if the time parameter is within or close to this segment + * @return if the time parameter is within this segment */ - public boolean timeIsInsideOrNear(long videoTime, long nearThreshold) { - return (start - nearThreshold) <= videoTime && videoTime < (end + nearThreshold); - } - - /** - * @return if the time parameter is outside this segment - */ - public boolean timeIsOutside(long videoTime) { - return start < videoTime || end <= videoTime; + public boolean containsTime(long videoTime) { + return start <= videoTime && videoTime < end; } /** @@ -102,7 +99,7 @@ public class SponsorSegment implements Comparable { */ @NonNull public String getSkipButtonText() { - return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength()); + return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength()).toString(); } /** @@ -110,7 +107,7 @@ public class SponsorSegment implements Comparable { */ @NonNull public String getSkippedToastText() { - return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()); + return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength()).toString(); } @Override diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java index f235814a..cd17eb3e 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java @@ -65,19 +65,15 @@ public class SBRequester { JSONArray segment = obj.getJSONArray("segment"); final long start = (long) (segment.getDouble(0) * 1000); final long end = (long) (segment.getDouble(1) * 1000); - if ((end - start) < minSegmentDuration) - continue; - String categoryKey = obj.getString("category"); String uuid = obj.getString("UUID"); - boolean locked = obj.getInt("locked") == 1; - - SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(categoryKey); - if (segmentCategory == null) { + final boolean locked = obj.getInt("locked") == 1; + String categoryKey = obj.getString("category"); + SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey); + if (category == null) { LogHelper.printException(() -> "Received unknown category: " + categoryKey); // should never happen - } else if (segmentCategory.behaviour != CategoryBehaviour.IGNORE) { - SponsorSegment sponsorSegment = new SponsorSegment(segmentCategory, uuid, start, end, locked); - segments.add(sponsorSegment); + } else if ((end - start) >= minSegmentDuration || category == SegmentCategory.HIGHLIGHT) { + segments.add(new SponsorSegment(category, uuid, start, end, locked)); } } LogHelper.printDebug(() -> { diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java index 588abd98..65666da8 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SkipSponsorButton.java @@ -4,10 +4,8 @@ import static app.revanced.integrations.utils.ReVancedUtils.getResourceColor; import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimension; import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize; import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier; -import static app.revanced.integrations.utils.StringRef.str; import android.content.Context; -import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; @@ -16,9 +14,10 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; + import java.util.Objects; -import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.sponsorblock.SegmentPlaybackController; import app.revanced.integrations.sponsorblock.objects.SponsorSegment; import app.revanced.integrations.utils.LogHelper; @@ -27,9 +26,9 @@ public class SkipSponsorButton extends FrameLayout { private static final boolean highContrast = true; private final LinearLayout skipSponsorBtnContainer; private final TextView skipSponsorTextView; - private final CharSequence skipSponsorTextCompact; private final Paint background; private final Paint border; + private SponsorSegment segment; final int defaultBottomMargin; final int ctaBottomMargin; @@ -61,11 +60,9 @@ public class SkipSponsorButton extends FrameLayout { skipSponsorTextView = Objects.requireNonNull((TextView) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_text", "id"))); // id:skip_ad_button_text; defaultBottomMargin = getResourceDimensionPixelSize("skip_button_default_bottom_margin"); // dimen:skip_button_default_bottom_margin ctaBottomMargin = getResourceDimensionPixelSize("skip_button_cta_bottom_margin"); // dimen:skip_button_cta_bottom_margin - skipSponsorTextCompact = str("sb_skip_button_compact"); // string:skip_ads "Skip ads" skipSponsorBtnContainer.setOnClickListener(v -> { - LogHelper.printDebug(() -> "Skip button clicked"); - SegmentPlaybackController.onSkipSponsorClicked(); + SegmentPlaybackController.onSkipSegmentClicked(segment); }); } @@ -90,10 +87,9 @@ public class SkipSponsorButton extends FrameLayout { /** * @return true, if this button state was changed */ - public boolean updateSkipButtonText(SponsorSegment segment) { - CharSequence newText = SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean() - ? skipSponsorTextCompact - : segment.getSkipButtonText(); + public boolean updateSkipButtonText(@NonNull SponsorSegment segment) { + this.segment = segment; + CharSequence newText = segment.getSkipButtonText(); if (newText.equals(skipSponsorTextView.getText())) { return false; } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java index 834dbc2f..5925f80c 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/ui/SponsorBlockViewController.java @@ -23,9 +23,13 @@ import app.revanced.integrations.utils.ReVancedUtils; public class SponsorBlockViewController { private static WeakReference inlineSponsorOverlayRef = new WeakReference<>(null); private static WeakReference youtubeOverlaysLayoutRef = new WeakReference<>(null); + private static WeakReference skipHighlightButtonRef = new WeakReference<>(null); private static WeakReference skipSponsorButtonRef = new WeakReference<>(null); private static WeakReference newSegmentLayoutRef = new WeakReference<>(null); - private static boolean canShowViewElements = true; + private static boolean canShowViewElements; + private static boolean newSegmentLayoutVisible; + @Nullable + private static SponsorSegment skipHighlight; @Nullable private static SponsorSegment skipSegment; @@ -51,161 +55,155 @@ public class SponsorBlockViewController { try { LogHelper.printDebug(() -> "initializing"); - RelativeLayout layout = new RelativeLayout(ReVancedUtils.getContext()); + Context context = ReVancedUtils.getContext(); + RelativeLayout layout = new RelativeLayout(context); layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT)); - LayoutInflater.from(ReVancedUtils.getContext()).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout); + LayoutInflater.from(context).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout); inlineSponsorOverlayRef = new WeakReference<>(layout); ViewGroup viewGroup = (ViewGroup) obj; - viewGroup.addView(layout, viewGroup.getChildCount() - 2); + viewGroup.addView(layout); + viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { + @Override + public void onChildViewAdded(View parent, View child) { + // ensure SB buttons and controls are always on top, otherwise the endscreen cards can cover the skip button + RelativeLayout layout = inlineSponsorOverlayRef.get(); + if (layout != null) { + layout.bringToFront(); + } + } + @Override + public void onChildViewRemoved(View parent, View child) { + } + }); youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup); + skipHighlightButtonRef = new WeakReference<>( + Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_highlight_button", "id")))); skipSponsorButtonRef = new WeakReference<>( Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_sponsor_button", "id")))); - newSegmentLayoutRef = new WeakReference<>( Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_new_segment_view", "id")))); + + newSegmentLayoutVisible = false; + skipHighlight = null; + skipSegment = null; } catch (Exception ex) { LogHelper.printException(() -> "initialize failure", ex); } } - public static void showSkipButton(@NonNull SponsorSegment info) { - skipSegment = Objects.requireNonNull(info); - updateSkipButton(); + public static void hideAll() { + hideSkipHighlightButton(); + hideSkipSegmentButton(); + hideNewSegmentLayout(); } - public static void hideSkipButton() { - skipSegment = null; - updateSkipButton(); - } - - private static void updateSkipButton() { - SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); - if (skipSponsorButton == null) { - return; - } - if (skipSegment == null) { - setSkipSponsorButtonVisibility(false); - } else { - final boolean layoutNeedsUpdating = skipSponsorButton.updateSkipButtonText(skipSegment); - if (layoutNeedsUpdating) { - bringLayoutToFront(); - } - setSkipSponsorButtonVisibility(true); - } - } - - public static void showNewSegmentLayout() { - setNewSegmentLayoutVisibility(true); - } - - public static void hideNewSegmentLayout() { + public static void showSkipHighlightButton(@NonNull SponsorSegment segment) { + skipHighlight = Objects.requireNonNull(segment); NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); - if (newSegmentLayout == null) { + // don't show highlight button if create new segment is visible + final boolean buttonVisibility = newSegmentLayout != null && newSegmentLayout.getVisibility() != View.VISIBLE; + updateSkipButton(skipHighlightButtonRef.get(), segment, buttonVisibility); + } + public static void showSkipSegmentButton(@NonNull SponsorSegment segment) { + skipSegment = Objects.requireNonNull(segment); + updateSkipButton(skipSponsorButtonRef.get(), segment, true); + } + + public static void hideSkipHighlightButton() { + skipHighlight = null; + updateSkipButton(skipHighlightButtonRef.get(), null, false); + } + public static void hideSkipSegmentButton() { + skipSegment = null; + updateSkipButton(skipSponsorButtonRef.get(), null, false); + } + + private static void updateSkipButton(@Nullable SkipSponsorButton button, + @Nullable SponsorSegment segment, boolean visible) { + if (button == null) { return; } - setNewSegmentLayoutVisibility(false); + if (segment != null) { + button.updateSkipButtonText(segment); + } + setViewVisibility(button, visible); } public static void toggleNewSegmentLayoutVisibility() { NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); - if (newSegmentLayout == null) { + if (newSegmentLayout == null) { // should never happen LogHelper.printException(() -> "toggleNewSegmentLayoutVisibility failure"); return; } - setNewSegmentLayoutVisibility(newSegmentLayout.getVisibility() == View.VISIBLE ? false : true); + newSegmentLayoutVisible = (newSegmentLayout.getVisibility() != View.VISIBLE); + if (skipHighlight != null) { + setViewVisibility(skipHighlightButtonRef.get(), !newSegmentLayoutVisible); + } + setViewVisibility(newSegmentLayout, newSegmentLayoutVisible); } - private static void playerTypeChanged(PlayerType playerType) { + public static void hideNewSegmentLayout() { + newSegmentLayoutVisible = false; + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + if (newSegmentLayout == null) { + return; + } + setViewVisibility(newSegmentLayout, false); + } + + private static void setViewVisibility(@Nullable View view, boolean visible) { + if (view == null) { + return; + } + visible &= canShowViewElements; + final int desiredVisibility = visible ? View.VISIBLE : View.GONE; + if (view.getVisibility() != desiredVisibility) { + view.setVisibility(desiredVisibility); + } + } + + private static void playerTypeChanged(@NonNull PlayerType playerType) { try { final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN; canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED); - setSkipButtonMargins(isWatchFullScreen); - setNewSegmentLayoutMargins(isWatchFullScreen); - updateSkipButton(); + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + setNewSegmentLayoutMargins(newSegmentLayout, isWatchFullScreen); + setViewVisibility(newSegmentLayoutRef.get(), newSegmentLayoutVisible); + + SkipSponsorButton skipHighlightButton = skipHighlightButtonRef.get(); + setSkipButtonMargins(skipHighlightButton, isWatchFullScreen); + setViewVisibility(skipHighlightButton, skipHighlight != null); + + SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); + setSkipButtonMargins(skipSponsorButton, isWatchFullScreen); + setViewVisibility(skipSponsorButton, skipSegment != null); } catch (Exception ex) { - LogHelper.printException(() -> "Player type changed error", ex); + LogHelper.printException(() -> "Player type changed failure", ex); } } - private static void setSkipButtonMargins(boolean fullScreen) { - SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); - if (skipSponsorButton == null) { - LogHelper.printException(() -> "setSkipButtonMargins failure"); - return; - } - - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams(); - if (params == null) { - LogHelper.printException(() -> "setSkipButtonMargins failure"); - return; - } - params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin; - skipSponsorButton.setLayoutParams(params); - } - - private static void setSkipSponsorButtonVisibility(boolean visible) { - SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); - if (skipSponsorButton == null) { - LogHelper.printException(() -> "setSkipSponsorButtonVisibility failure"); - return; - } - - visible &= canShowViewElements; - - final int desiredVisibility = visible ? View.VISIBLE : View.GONE; - if (skipSponsorButton.getVisibility() != desiredVisibility) { - skipSponsorButton.setVisibility(desiredVisibility); - if (visible) { - bringLayoutToFront(); - } + private static void setNewSegmentLayoutMargins(@Nullable NewSegmentLayout layout, boolean fullScreen) { + if (layout != null) { + setLayoutMargins(layout, fullScreen, layout.defaultBottomMargin, layout.ctaBottomMargin); } } - - private static void setNewSegmentLayoutMargins(boolean fullScreen) { - NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); - if (newSegmentLayout == null) { - LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (button is null)"); - return; + private static void setSkipButtonMargins(@Nullable SkipSponsorButton button, boolean fullScreen) { + if (button != null) { + setLayoutMargins(button, fullScreen, button.defaultBottomMargin, button.ctaBottomMargin); } - - RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams(); + } + private static void setLayoutMargins(@NonNull View view, boolean fullScreen, + int defaultBottomMargin, int ctaBottomMargin) { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams(); if (params == null) { LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)"); return; } - params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin; - newSegmentLayout.setLayoutParams(params); - } - - private static void setNewSegmentLayoutVisibility(boolean visible) { - NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); - if (newSegmentLayout == null) { - LogHelper.printException(() -> "setNewSegmentLayoutVisibility failure"); - return; - } - - visible &= canShowViewElements; - - final int desiredVisibility = visible ? View.VISIBLE : View.GONE; - if (newSegmentLayout.getVisibility() != desiredVisibility) { - newSegmentLayout.setVisibility(desiredVisibility); - if (visible) { - bringLayoutToFront(); - } - } - } - - private static void bringLayoutToFront() { - RelativeLayout layout = inlineSponsorOverlayRef.get(); - if (layout != null) { - // needed to keep skip button overtop end screen cards - layout.bringToFront(); - layout.requestLayout(); - layout.invalidate(); - } + params.bottomMargin = fullScreen ? ctaBottomMargin : defaultBottomMargin; + view.setLayoutParams(params); } /** From 4d9b41ca3a60de2bace9e3d5a0df90d0fb64c5d2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 16 Apr 2023 18:20:16 +0000 Subject: [PATCH 20/30] chore(release): 0.103.0-dev.2 [skip ci] # [0.103.0-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.1...v0.103.0-dev.2) (2023-04-16) ### Features * **youtube/sponsorblock:** skip to video highlight ([#352](https://github.com/revanced/revanced-integrations/issues/352)) ([03f09cf](https://github.com/revanced/revanced-integrations/commit/03f09cf7bce1747b1d402f3a3e16dd69c364dfce)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d52db48b..c29f0e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [0.103.0-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.1...v0.103.0-dev.2) (2023-04-16) + + +### Features + +* **youtube/sponsorblock:** skip to video highlight ([#352](https://github.com/revanced/revanced-integrations/issues/352)) ([03f09cf](https://github.com/revanced/revanced-integrations/commit/03f09cf7bce1747b1d402f3a3e16dd69c364dfce)) + # [0.103.0-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.102.0...v0.103.0-dev.1) (2023-04-16) diff --git a/gradle.properties b/gradle.properties index 81307db1..acf51305 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.103.0-dev.1 +version = 0.103.0-dev.2 From da7b669c97d18bef51de98ddfde8a514ebb61ecf Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 18 Apr 2023 01:55:33 +0200 Subject: [PATCH 21/30] feat(youtube/general-ads): hide new type of movie offer ad --- .../app/revanced/integrations/patches/GeneralAdsPatch.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index d4a346ca..87a4166f 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -74,7 +74,8 @@ public final class GeneralAdsPatch extends Filter { "compact_movie", "horizontal_movie_shelf", "movie_and_show_upsell_card", - "compact_tvfilm_item" + "compact_tvfilm_item", + "offer_module_root ); this.pathRegister.registerAll( From fb8442823e295884ef5b722abadcfe3cc2903179 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 17 Apr 2023 23:57:37 +0000 Subject: [PATCH 22/30] chore(release): 0.103.0-dev.3 [skip ci] # [0.103.0-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.2...v0.103.0-dev.3) (2023-04-17) ### Features * **youtube/general-ads:** hide new type of movie offer ad ([da7b669](https://github.com/revanced/revanced-integrations/commit/da7b669c97d18bef51de98ddfde8a514ebb61ecf)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c29f0e8f..8742f527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [0.103.0-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.2...v0.103.0-dev.3) (2023-04-17) + + +### Features + +* **youtube/general-ads:** hide new type of movie offer ad ([da7b669](https://github.com/revanced/revanced-integrations/commit/da7b669c97d18bef51de98ddfde8a514ebb61ecf)) + # [0.103.0-dev.2](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.1...v0.103.0-dev.2) (2023-04-16) diff --git a/gradle.properties b/gradle.properties index acf51305..2221b8cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.103.0-dev.2 +version = 0.103.0-dev.3 From 8797765efa0fb98b6e11a7198ecce3943df3daf5 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 18 Apr 2023 01:59:32 +0200 Subject: [PATCH 23/30] fix: minor syntax error --- .../java/app/revanced/integrations/patches/GeneralAdsPatch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index 87a4166f..e62d120d 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -75,7 +75,7 @@ public final class GeneralAdsPatch extends Filter { "horizontal_movie_shelf", "movie_and_show_upsell_card", "compact_tvfilm_item", - "offer_module_root + "offer_module_root" ); this.pathRegister.registerAll( From 48050c1c5085cb8e28f58ee6944212af15f225ff Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 18 Apr 2023 00:01:33 +0000 Subject: [PATCH 24/30] chore(release): 0.103.0-dev.4 [skip ci] # [0.103.0-dev.4](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.3...v0.103.0-dev.4) (2023-04-18) ### Bug Fixes * minor syntax error ([8797765](https://github.com/revanced/revanced-integrations/commit/8797765efa0fb98b6e11a7198ecce3943df3daf5)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8742f527..70b56b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [0.103.0-dev.4](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.3...v0.103.0-dev.4) (2023-04-18) + + +### Bug Fixes + +* minor syntax error ([8797765](https://github.com/revanced/revanced-integrations/commit/8797765efa0fb98b6e11a7198ecce3943df3daf5)) + # [0.103.0-dev.3](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.2...v0.103.0-dev.3) (2023-04-17) diff --git a/gradle.properties b/gradle.properties index 2221b8cb..b015904c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.103.0-dev.3 +version = 0.103.0-dev.4 From 41c07f77f47d726fdc16120bb5695407a7dec1fc Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 19 Apr 2023 10:36:10 +0400 Subject: [PATCH 25/30] fix(youtube/return-youtube-dislike): render dislikes when scrolling into the screen (#350) Co-authored-by: oSumAtrIX --- .../patches/ReturnYouTubeDislikePatch.java | 96 ++++-- .../ReturnYouTubeDislike.java | 300 +++++++----------- .../requests/ReturnYouTubeDislikeApi.java | 25 +- 3 files changed, 212 insertions(+), 209 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java index dd1d6e37..bbadf90d 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ReturnYouTubeDislikePatch.java @@ -1,48 +1,108 @@ package app.revanced.integrations.patches; +import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote; + +import android.text.SpannableString; import android.text.Spanned; -import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; + +import androidx.annotation.NonNull; import java.util.concurrent.atomic.AtomicReference; -/** - * TODO: delete this empty class, and point the patch to {@link ReturnYouTubeDislike} - */ +import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; + public class ReturnYouTubeDislikePatch { /** - * Injection point + * Injection point. */ public static void newVideoLoaded(String videoId) { - ReturnYouTubeDislike.newVideoLoaded(videoId); + try { + if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; + ReturnYouTubeDislike.newVideoLoaded(videoId); + } catch (Exception ex) { + LogHelper.printException(() -> "newVideoLoaded failure", ex); + } } /** - * Injection point + * Injection point. * - * Called when a litho text component is created + * Called when a litho text component is initially created, + * and also when a Span is later reused again (such as scrolling off/on screen). + * + * This method is sometimes called on the main thread, but it usually is called _off_ the main thread. + * This method can be called multiple times for the same UI element (including after dislikes was added). + * + * @param textRef Cache reference to the like/dislike char sequence, + * which may or may not be the same as the original span parameter. + * If dislikes are added, the atomic reference must be set to the replacement span. + * @param original Original span that was created or reused by Litho. + * @return The original span (if nothing should change), or a replacement span that contains dislikes. */ - public static void onComponentCreated(Object conversionContext, AtomicReference textRef) { - ReturnYouTubeDislike.onComponentCreated(conversionContext, textRef); + @NonNull + public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + @NonNull AtomicReference textRef, + @NonNull CharSequence original) { + try { + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { + return original; + } + SpannableString replacement = ReturnYouTubeDislike.getDislikeSpanForContext(conversionContext, original); + if (replacement != null) { + textRef.set(replacement); + return replacement; + } + } catch (Exception ex) { + LogHelper.printException(() -> "onComponentCreated AtomicReference failure", ex); + } + return original; } /** - * Injection point + * Injection point. * - * Called when a Shorts dislike Spannable is created + * Called when a Shorts dislike Spanned is created. */ - public static Spanned onShortsComponentCreated(Spanned dislike) { - return ReturnYouTubeDislike.onShortsComponentCreated(dislike); + public static Spanned onShortsComponentCreated(Spanned original) { + try { + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { + return original; + } + SpannableString replacement = ReturnYouTubeDislike.getDislikeSpanForShort(original); + if (replacement != null) { + return replacement; + } + } catch (Exception ex) { + LogHelper.printException(() -> "onShortsComponentCreated failure", ex); + } + return original; } /** - * Injection point + * Injection point. * - * Called when the like/dislike button is clicked + * Called when the user likes or dislikes. * - * @param vote -1 (dislike), 0 (none) or 1 (like) + * @param vote int that matches {@link ReturnYouTubeDislike.Vote#value} */ public static void sendVote(int vote) { - ReturnYouTubeDislike.sendVote(vote); + try { + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { + return; + } + + for (Vote v : Vote.values()) { + if (v.value == vote) { + ReturnYouTubeDislike.sendVote(v); + return; + } + } + LogHelper.printException(() -> "Unknown vote type: " + vote); + } catch (Exception ex) { + LogHelper.printException(() -> "sendVote failure", ex); + } } } diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index d441c2b1..b256964b 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -31,7 +31,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData; import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; @@ -41,10 +40,13 @@ import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; import app.revanced.integrations.utils.ThemeHelper; +/** + * Because Litho creates spans using multiple threads, this entire class supports multithreading as well. + */ public class ReturnYouTubeDislike { /** * Maximum amount of time to block the UI from updates while waiting for network call to complete. - *

+ * * Must be less than 5 seconds, as per: * https://developer.android.com/topic/performance/vitals/anr */ @@ -52,18 +54,17 @@ public class ReturnYouTubeDislike { /** * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. - * Can be any almost any non-visible character + * Can be any almost any non-visible character. */ private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character /** - * Used to send votes, one by one, in the same order the user created them + * Used to send votes, one by one, in the same order the user created them. */ private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor(); /** - * Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}, - * as multiple threads access this class. + * Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}. */ private static final Object videoIdLockObject = new Object(); @@ -71,14 +72,13 @@ public class ReturnYouTubeDislike { @GuardedBy("videoIdLockObject") private static String currentVideoId; - /** - * If {@link #currentVideoId} and the RYD data is for the last shorts loaded + * If {@link #currentVideoId} and the RYD data is for the last shorts loaded. */ private static volatile boolean lastVideoLoadedWasShort; /** - * Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes + * Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes. */ @Nullable @GuardedBy("videoIdLockObject") @@ -86,19 +86,31 @@ public class ReturnYouTubeDislike { /** * Original dislike span, before modifications. - * Required for segmented layout */ @Nullable @GuardedBy("videoIdLockObject") private static Spanned originalDislikeSpan; /** - * Replacement like/dislike span that includes formatted dislikes and is ready to display + * Replacement like/dislike span that includes formatted dislikes. + * Used to prevent recreating the same span multiple times. */ @Nullable @GuardedBy("videoIdLockObject") private static SpannableString replacementLikeDislikeSpan; + /** + * For formatting dislikes as number. + */ + @GuardedBy("ReturnYouTubeDislike.class") // not thread safe + private static CompactDecimalFormat dislikeCountFormatter; + + /** + * For formatting dislikes as percentage. + */ + @GuardedBy("ReturnYouTubeDislike.class") + private static NumberFormat dislikePercentageFormatter; + public enum Vote { LIKE(1), DISLIKE(-1), @@ -114,18 +126,6 @@ public class ReturnYouTubeDislike { private ReturnYouTubeDislike() { } // only static methods - /** - * Used to format like/dislike count. - */ - @GuardedBy("ReturnYouTubeDislike.class") // not thread safe - private static CompactDecimalFormat dislikeCountFormatter; - - /** - * Used to format like/dislike count. - */ - @GuardedBy("ReturnYouTubeDislike.class") - private static NumberFormat dislikePercentageFormatter; - public static void onEnabledChange(boolean enabled) { if (!enabled) { // Must clear old values, to protect against using stale data @@ -148,7 +148,7 @@ public class ReturnYouTubeDislike { } /** - * Should be called if user changes settings for dislikes appearance. + * Should be called after a user dislikes, or if the user changes settings for dislikes appearance. */ public static void clearCache() { synchronized (videoIdLockObject) { @@ -174,146 +174,113 @@ public class ReturnYouTubeDislike { } public static void newVideoLoaded(@NonNull String videoId) { - if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; + Objects.requireNonNull(videoId); - try { - Objects.requireNonNull(videoId); - - PlayerType currentPlayerType = PlayerType.getCurrent(); - if (currentPlayerType == PlayerType.INLINE_MINIMAL) { - LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId); + PlayerType currentPlayerType = PlayerType.getCurrent(); + if (currentPlayerType == PlayerType.INLINE_MINIMAL) { + LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId); + setCurrentVideoId(null); + return; + } + synchronized (videoIdLockObject) { + if (videoId.equals(currentVideoId)) { + return; // already loaded + } + if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id + LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId); setCurrentVideoId(null); return; } - synchronized (videoIdLockObject) { - if (videoId.equals(currentVideoId)) { - return; // already loaded - } - if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id - LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId); - setCurrentVideoId(null); - return; - } - LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType); - setCurrentVideoId(videoId); + LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType); + setCurrentVideoId(videoId); - // If a Short is opened while a regular video is on screen, this will incorrectly set this as false. - // But this check is needed to fix unusual situations of opening/closing the app - // while both a regular video and a short are on screen. - lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden(); + // If a Short is opened while a regular video is on screen, this will incorrectly set this as false. + // But this check is needed to fix unusual situations of opening/closing the app + // while both a regular video and a short are on screen. + lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden(); - // no need to wrap the call in a try/catch, - // as any exceptions are propagated out in the later Future#Get call - voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); - } - } catch (Exception ex) { - LogHelper.printException(() -> "Failed to load new video: " + videoId, ex); + // No need to wrap the call in a try/catch, + // as any exceptions are propagated out in the later Future#Get call. + voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); } } /** - * Called when a litho text component is created. - * - * This method is sometimes called on the main thread, but it usually is called _off_ the main thread. - * This method can be called multiple times for the same UI element (including after dislikes was added) - * - * @param textRef atomic reference should always be non null, but the spanned reference inside can be null. + * @return NULL if the span does not need changing or if RYD is not available. */ - public static void onComponentCreated(@NonNull Object conversionContext, @NonNull AtomicReference textRef) { - try { - if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; - - if (PlayerType.getCurrent().isNoneOrHidden()) { - return; - } - - String conversionContextString = conversionContext.toString(); - final boolean isSegmentedButton; - if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) { - isSegmentedButton = true; - } else if (conversionContextString.contains("|dislike_button.eml|")) { - isSegmentedButton = false; - } else { - return; - } - - if (lastVideoLoadedWasShort) { - // user: - // 1, opened a video - // 2. opened a short (without closing the regular video) - // 3. closed the short - // 4. regular video is now present, but the videoId and RYD data is still for the short - LogHelper.printDebug(() -> "Ignoring onComponentCreated(), as data loaded is is for prior short"); - return; - } - - Spanned replacement = waitForFetchAndUpdateReplacementSpan((Spanned) textRef.get(), isSegmentedButton); - if (replacement != null) { - textRef.set(replacement); - } - } catch (Exception ex) { - LogHelper.printException(() -> "onComponentCreated failure", ex); + @Nullable + public static SpannableString getDislikeSpanForContext(@NonNull Object conversionContext, @NonNull CharSequence original) { + if (PlayerType.getCurrent().isNoneOrHidden()) { + return null; } + String conversionContextString = conversionContext.toString(); + final boolean isSegmentedButton; + if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) { + isSegmentedButton = true; + } else if (conversionContextString.contains("|dislike_button.eml|")) { + isSegmentedButton = false; + } else { + return null; + } + + if (lastVideoLoadedWasShort) { + // user: + // 1, opened a video + // 2. opened a short (without closing the regular video) + // 3. closed the short + // 4. regular video is now present, but the videoId and RYD data is still for the short + LogHelper.printDebug(() -> "Ignoring getDislikeSpanForContext(), as data loaded is for prior short"); + return null; + } + + return waitForFetchAndUpdateReplacementSpan((Spannable) original, isSegmentedButton); } /** * Called when a Shorts dislike Spannable is created. */ - public static Spanned onShortsComponentCreated(Spanned original) { - try { - if (SettingsEnum.RYD_ENABLED.getBoolean()) { - lastVideoLoadedWasShort = true; // it's now certain the video and data are a short - Spanned replacement = waitForFetchAndUpdateReplacementSpan(original, false); - if (replacement != null) { - return replacement; - } - } - } catch (Exception ex) { - LogHelper.printException(() -> "onShortsComponentCreated failure", ex); - } - return original; + public static SpannableString getDislikeSpanForShort(@NonNull Spanned original) { + lastVideoLoadedWasShort = true; // it's now certain the video and data are a short + return waitForFetchAndUpdateReplacementSpan(original, false); } - // alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick + // Alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick. private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) { return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1; } /** - * @return NULL if the span does not need changing or if RYD is not available + * @return NULL if the span does not need changing or if RYD is not available. */ @Nullable - private static SpannableString waitForFetchAndUpdateReplacementSpan(@Nullable Spanned oldSpannable, boolean isSegmentedButton) { - if (oldSpannable == null) { - LogHelper.printDebug(() -> "Cannot add dislikes (injection code was called with null Span)"); - return null; - } + private static SpannableString waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) { try { synchronized (videoIdLockObject) { - if (oldSpannable.equals(replacementLikeDislikeSpan)) { - LogHelper.printDebug(() -> "Ignoring span that already contains dislikes"); - return null; - } if (replacementLikeDislikeSpan != null) { - LogHelper.printDebug(() -> "Using previously created dislike span"); - return replacementLikeDislikeSpan; - } - if (isSegmentedButton) { - if (isPreviouslyCreatedSegmentedSpan(oldSpannable)) { - // need to recreate using original, as oldSpannable has prior outdated dislike values - oldSpannable = originalDislikeSpan; - if (oldSpannable == null) { - LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen - return null; - } - } else { - originalDislikeSpan = oldSpannable; // most up to date original + String oldSpannableString = oldSpannable.toString(); + if (replacementLikeDislikeSpan.toString().equals(oldSpannableString)) { + LogHelper.printDebug(() -> "Ignoring previously created dislikes span"); + return null; } + if (originalDislikeSpan.toString().equals(oldSpannableString)) { + LogHelper.printDebug(() -> "Replacing span with previously created dislike span"); + return replacementLikeDislikeSpan; + } + } + if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(oldSpannable)) { + // need to recreate using original, as oldSpannable has prior outdated dislike values + oldSpannable = originalDislikeSpan; + if (oldSpannable == null) { + LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen + return null; + } + } else { + originalDislikeSpan = oldSpannable; // most up to date original } } - // Must block the current thread until fetching is done - // There's no known way to edit the text after creation yet + // Must block the current thread until fetching is done. + // There's no known way to edit the text after creation yet. Future fetchFuture = getVoteFetchFuture(); if (fetchFuture == null) { LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)"); @@ -340,37 +307,16 @@ public class ReturnYouTubeDislike { return null; } - /** - * Called when the like/dislike button is clicked. - * - * @param vote int that matches {@link Vote#value} - */ - public static void sendVote(int vote) { - if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; - - try { - for (Vote v : Vote.values()) { - if (v.value == vote) { - sendVote(v); - return; - } - } - LogHelper.printException(() -> "Unknown vote type: " + vote); - } catch (Exception ex) { - LogHelper.printException(() -> "sendVote failure", ex); - } - } - - private static void sendVote(@NonNull Vote vote) { + public static void sendVote(@NonNull Vote vote) { ReVancedUtils.verifyOnMainThread(); Objects.requireNonNull(vote); try { - // Must make a local copy of videoId, since it may change between now and when the vote thread runs + // Must make a local copy of videoId, since it may change between now and when the vote thread runs. String videoIdToVoteFor = getCurrentVideoId(); if (videoIdToVoteFor == null || lastVideoLoadedWasShort != PlayerType.getCurrent().isNoneOrHidden()) { // User enabled RYD after starting playback of a video. // Or shorts was loaded with regular video present, then shorts was closed, - // and then user voted on the now visible original video + // and then user voted on the now visible original video. // Cannot send a vote, because the loaded videoId is for the wrong video. ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted")); return; @@ -387,15 +333,15 @@ public class ReturnYouTubeDislike { } }); - clearCache(); // ui values need updating + clearCache(); // UI needs updating - // update the downloaded vote data + // Update the downloaded vote data. Future future = getVoteFetchFuture(); if (future == null) { LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null"); return; } - // the future should always be completed before user can like/dislike, but use a timeout just in case + // The future should always be completed before user can like/dislike, but use a timeout just in case. RYDVoteData voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS); if (voteData == null) { // RYD fetch failed @@ -409,10 +355,10 @@ public class ReturnYouTubeDislike { } /** - * Must call off main thread, as this will make a network call if user is not yet registered + * Must call off main thread, as this will make a network call if user is not yet registered. * * @return ReturnYouTubeDislike user ID. If user registration has never happened - * and the network call fails, this returns NULL + * and the network call fails, this returns NULL. */ @Nullable private static String getUserId() { @@ -423,7 +369,7 @@ public class ReturnYouTubeDislike { return userId; } - userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed + userId = ReturnYouTubeDislikeApi.registerAsNewUser(); if (userId != null) { SettingsEnum.RYD_USER_ID.saveValue(userId); } @@ -431,25 +377,25 @@ public class ReturnYouTubeDislike { } /** - * @param isSegmentedButton if UI is using the segmented single UI component for both like and dislike + * @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike. */ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) { if (!isSegmentedButton) { - // simple replacement of 'dislike' with a number/percentage + // Simple replacement of 'dislike' with a number/percentage. return newSpannableWithDislikes(oldSpannable, voteData); } - // note: some locales use right to left layout (arabic, hebrew, etc), - // and care must be taken to retain the existing RTL encoding character on the likes string - // otherwise text will incorrectly show as left to right - // if making changes to this code, change device settings to a RTL language and verify layout is correct + // Note: Some locales use right to left layout (arabic, hebrew, etc), + // and care must be taken to retain the existing RTL encoding character on the likes string, + // otherwise text will incorrectly show as left to right. + // If making changes to this code, change device settings to a RTL language and verify layout is correct. String oldLikesString = oldSpannable.toString(); // YouTube creators can hide the like count on a video, - // and the like count appears as a device language specific string that says 'Like' - // check if the string contains any numbers + // and the like count appears as a device language specific string that says 'Like'. + // Check if the string contains any numbers. if (!stringContainsNumber(oldLikesString)) { - // likes are hidden. + // Likes are hidden. // RYD does not provide usable data for these types of videos, // and the API returns bogus data (zero likes and zero dislikes) // discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530 @@ -457,7 +403,7 @@ public class ReturnYouTubeDislike { // example video: https://www.youtube.com/watch?v=UnrU5vxCHxw // RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw // - // Change the "Likes" string to show that likes and dislikes are hidden + // Change the "Likes" string to show that likes and dislikes are hidden. String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner"); return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString); } @@ -467,7 +413,7 @@ public class ReturnYouTubeDislike { final int separatorColor = ThemeHelper.isDarkTheme() ? 0x29AAAAAA // transparent dark gray : 0xFFD9D9D9; // light gray - DisplayMetrics dp = ReVancedUtils.getContext().getResources().getDisplayMetrics(); + DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics(); if (!compactLayout) { // left separator @@ -511,9 +457,9 @@ public class ReturnYouTubeDislike { } /** - * Correctly handles any unicode numbers (such as Arabic numbers) + * Correctly handles any unicode numbers (such as Arabic numbers). * - * @return if the string contains at least 1 number + * @return if the string contains at least 1 number. */ private static boolean stringContainsNumber(@NonNull String text) { for (int index = 0, length = text.length(); index < length; index++) { @@ -545,10 +491,10 @@ public class ReturnYouTubeDislike { synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize if (dislikeCountFormatter == null) { // Note: Java number formatters will use the locale specific number characters. - // such as Arabic which formats "1.2" into "١٫٢" + // such as Arabic which formats "1.234" into "۱,۲۳٤" // But YouTube disregards locale specific number characters // and instead shows english number characters everywhere. - Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale; + Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale; LogHelper.printDebug(() -> "Locale: " + locale); dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT); } @@ -563,7 +509,7 @@ public class ReturnYouTubeDislike { private static String formatDislikePercentage(float dislikePercentage) { synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize if (dislikePercentageFormatter == null) { - Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale; + Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale; LogHelper.printDebug(() -> "Locale: " + locale); dislikePercentageFormatter = NumberFormat.getPercentInstance(locale); } diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index ae154cfe..970f3e48 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -203,7 +203,7 @@ public class ReturnYouTubeDislikeApi { @SuppressWarnings("NonAtomicOperationOnVolatileField") // do not want to pay performance cost of full synchronization for debug fields that are only estimates anyways private static void updateStatistics(long timeNetworkCallStarted, long timeNetworkCallEnded, boolean connectionError, boolean rateLimitHit) { if (connectionError && rateLimitHit) { - throw new IllegalArgumentException("both connection error and rate limit parameter were true"); + throw new IllegalArgumentException(); } final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted; fetchCallResponseTimeTotal += responseTimeOfFetchCall; @@ -320,7 +320,7 @@ public class ReturnYouTubeDislikeApi { return confirmRegistration(userId, solution); } LogHelper.printException(() -> "Failed to register new user: " + userId - + " response code was: " + responseCode); + + " response code was: " + responseCode); // failed attempt, and ok to log userId connection.disconnect(); } catch (Exception ex) { LogHelper.printException(() -> "Failed to register user", ex); @@ -337,7 +337,7 @@ public class ReturnYouTubeDislikeApi { if (checkIfRateLimitInEffect("confirmRegistration")) { return null; } - LogHelper.printDebug(() -> "Trying to confirm registration for user: " + userId + " with solution: " + solution); + LogHelper.printDebug(() -> "Trying to confirm registration with solution: " + solution); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId); applyCommonPostRequestSettings(connection); @@ -355,7 +355,7 @@ public class ReturnYouTubeDislikeApi { if (responseCode == HTTP_STATUS_CODE_SUCCESS) { String result = Requester.parseJson(connection); if (result.equalsIgnoreCase("true")) { - LogHelper.printDebug(() -> "Registration confirmation successful for user: " + userId); + LogHelper.printDebug(() -> "Registration confirmation successful"); return userId; } LogHelper.printException(() -> "Failed to confirm registration for user: " + userId @@ -382,8 +382,7 @@ public class ReturnYouTubeDislikeApi { if (checkIfRateLimitInEffect("sendVote")) { return false; } - LogHelper.printDebug(() -> "Trying to vote for video: " - + videoId + " with vote: " + vote + " user: " + userId); + LogHelper.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE); applyCommonPostRequestSettings(connection); @@ -408,11 +407,10 @@ public class ReturnYouTubeDislikeApi { return confirmVote(videoId, userId, solution); } LogHelper.printException(() -> "Failed to send vote for video: " + videoId - + " userId: " + userId + " vote: " + vote + " response code was: " + responseCode); + + " vote: " + vote + " response code was: " + responseCode); connection.disconnect(); // something went wrong, might as well disconnect } catch (Exception ex) { - LogHelper.printException(() -> "Failed to send vote for video: " + videoId - + " user: " + userId + " vote: " + vote, ex); + LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); } return false; } @@ -427,8 +425,7 @@ public class ReturnYouTubeDislikeApi { if (checkIfRateLimitInEffect("confirmVote")) { return false; } - LogHelper.printDebug(() -> "Trying to confirm vote for video: " - + videoId + " user: " + userId + " solution: " + solution); + LogHelper.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); applyCommonPostRequestSettings(connection); @@ -450,15 +447,15 @@ public class ReturnYouTubeDislikeApi { return true; } LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId - + " user: " + userId + " solution: " + solution + " response string was: " + result); + + " solution: " + solution + " response string was: " + result); } else { LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId - + " user: " + userId + " solution: " + solution + " response code was: " + responseCode); + + " solution: " + solution + " response code was: " + responseCode); } connection.disconnect(); // something went wrong, might as well disconnect } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId - + " user: " + userId + " solution: " + solution, ex); + + " solution: " + solution, ex); } return false; } From 2ea55af9ce4baf77a14dca04975fe24b30035343 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 19 Apr 2023 06:37:56 +0000 Subject: [PATCH 26/30] chore(release): 0.103.0-dev.5 [skip ci] # [0.103.0-dev.5](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.4...v0.103.0-dev.5) (2023-04-19) ### Bug Fixes * **youtube/return-youtube-dislike:** render dislikes when scrolling into the screen ([#350](https://github.com/revanced/revanced-integrations/issues/350)) ([41c07f7](https://github.com/revanced/revanced-integrations/commit/41c07f77f47d726fdc16120bb5695407a7dec1fc)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b56b17..91572c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [0.103.0-dev.5](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.4...v0.103.0-dev.5) (2023-04-19) + + +### Bug Fixes + +* **youtube/return-youtube-dislike:** render dislikes when scrolling into the screen ([#350](https://github.com/revanced/revanced-integrations/issues/350)) ([41c07f7](https://github.com/revanced/revanced-integrations/commit/41c07f77f47d726fdc16120bb5695407a7dec1fc)) + # [0.103.0-dev.4](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.3...v0.103.0-dev.4) (2023-04-18) diff --git a/gradle.properties b/gradle.properties index b015904c..273ef628 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.103.0-dev.4 +version = 0.103.0-dev.5 From 86c27890ada8739ea272f8783eb4ef526b808a27 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 19 Apr 2023 13:30:46 +0400 Subject: [PATCH 27/30] fix(youtube/sponsorblock): always show the video time without segments using left to right layout (#359) --- .../SegmentPlaybackController.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java index 9b5e27c0..60efccf1 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/SegmentPlaybackController.java @@ -58,23 +58,23 @@ public class SegmentPlaybackController { /** * Because loading can take time, show the skip to highlight for a few seconds after the segments load. - * This is the end time (in milliseconds) to no longer show the initial display skip to highlight. + * This is the system time (in milliseconds) to no longer show the initial display skip to highlight. */ private static long highlightSegmentInitialShowEndTime; /** - * Current (non-highlight) segment that user can manually skip + * Current (non-highlight) segment that user can manually skip. */ @Nullable private static SponsorSegment segmentCurrentlyPlaying; /** * Currently playing manual skip segment, that is scheduled to hide. - * This will always be NULL or equal to {@link #segmentCurrentlyPlaying} + * This will always be NULL or equal to {@link #segmentCurrentlyPlaying}. */ @Nullable private static SponsorSegment scheduledHideSegment; /** - * Upcoming segment that is scheduled to either autoskip or show the manual skip button + * Upcoming segment that is scheduled to either autoskip or show the manual skip button. */ @Nullable private static SponsorSegment scheduledUpcomingSegment; @@ -110,7 +110,7 @@ public class SegmentPlaybackController { } /** - * Clears all downloaded data + * Clears all downloaded data. */ private static void clearData() { currentVideoId = null; @@ -507,7 +507,7 @@ public class SegmentPlaybackController { } /** - * @param segment can be either a highlight or a regular manual skip segment + * @param segment can be either a highlight or a regular manual skip segment. */ public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) { try { @@ -524,7 +524,7 @@ public class SegmentPlaybackController { } /** - * Injection point + * Injection point. */ public static void setSponsorBarAbsoluteLeft(final Rect rect) { setSponsorBarAbsoluteLeft(rect.left); @@ -557,7 +557,7 @@ public class SegmentPlaybackController { } /** - * Injection point + * Injection point. */ public static void setSponsorBarAbsoluteRight(final Rect rect) { setSponsorBarAbsoluteRight(rect.right); @@ -589,13 +589,14 @@ public class SegmentPlaybackController { } /** - * Injection point + * Injection point. */ public static String appendTimeWithoutSegments(String totalTime) { try { if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() && !TextUtils.isEmpty(totalTime) && !TextUtils.isEmpty(timeWithoutSegments)) { - return totalTime + timeWithoutSegments; + // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages + return "\u202D" + totalTime + timeWithoutSegments; // u202D = left to right override } } catch (Exception ex) { LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex); @@ -631,13 +632,13 @@ public class SegmentPlaybackController { if (highlightSegmentTimeBarScreenWidth == -1) { highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH, - ReVancedUtils.getContext().getResources().getDisplayMetrics()); + Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics()); } return highlightSegmentTimeBarScreenWidth; } /** - * Injection point + * Injection point. */ public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { try { From f7c3543d4f04ed772e2263247960e382b43928e1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 19 Apr 2023 09:32:27 +0000 Subject: [PATCH 28/30] chore(release): 0.103.0-dev.6 [skip ci] # [0.103.0-dev.6](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.5...v0.103.0-dev.6) (2023-04-19) ### Bug Fixes * **youtube/sponsorblock:** always show the video time without segments using left to right layout ([#359](https://github.com/revanced/revanced-integrations/issues/359)) ([86c2789](https://github.com/revanced/revanced-integrations/commit/86c27890ada8739ea272f8783eb4ef526b808a27)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91572c56..e329e4e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [0.103.0-dev.6](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.5...v0.103.0-dev.6) (2023-04-19) + + +### Bug Fixes + +* **youtube/sponsorblock:** always show the video time without segments using left to right layout ([#359](https://github.com/revanced/revanced-integrations/issues/359)) ([86c2789](https://github.com/revanced/revanced-integrations/commit/86c27890ada8739ea272f8783eb4ef526b808a27)) + # [0.103.0-dev.5](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.4...v0.103.0-dev.5) (2023-04-19) diff --git a/gradle.properties b/gradle.properties index 273ef628..780bfd68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.103.0-dev.5 +version = 0.103.0-dev.6 From a2af2c0c9ffc2f961773bfb8d546aff68c2d1c27 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 20 Apr 2023 10:41:36 +0400 Subject: [PATCH 29/30] fix(youtube/hide-video-action-buttons): fix 'hide share button' (#360) --- .../integrations/patches/ButtonsPatch.java | 50 ++++--------------- .../integrations/settings/SettingsEnum.java | 6 +-- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java b/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java index 3ff3edf7..621b04c3 100644 --- a/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java @@ -1,60 +1,30 @@ package app.revanced.integrations.patches; import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; final class ButtonsPatch extends Filter { - private final BlockRule actionButtonsRule; - private final BlockRule dislikeRule; private final BlockRule actionBarRule; - private final BlockRule[] rules; - public ButtonsPatch() { - BlockRule like = new BlockRule(SettingsEnum.HIDE_LIKE_BUTTON, "|like_button"); - dislikeRule = new BlockRule(SettingsEnum.HIDE_DISLIKE_BUTTON, "dislike_button"); - BlockRule download = new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"); - actionButtonsRule = new BlockRule(SettingsEnum.HIDE_ACTION_BUTTON, "ContainerType|video_action_button"); - BlockRule playlist = new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"); - rules = new BlockRule[]{like, dislikeRule, download, actionButtonsRule, playlist}; - actionBarRule = new BlockRule(null, "video_action_bar"); - - this.pathRegister.registerAll( - like, - dislikeRule, - download, - playlist + pathRegister.registerAll( + new BlockRule(SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, "|like_button", "dislike_button"), + new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"), + new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"), + new BlockRule(SettingsEnum.HIDE_ACTION_BUTTONS, "ContainerType|video_action_button") ); } - private boolean hideActionBar() { - for (BlockRule rule : rules) if (!rule.isEnabled()) return false; + private boolean canHideActionBar() { + for (BlockRule rule : pathRegister) if (!rule.isEnabled()) return false; return true; } @Override public boolean filter(final String path, final String identifier) { - if (hideActionBar() && actionBarRule.check(identifier).isBlocked()) return true; + // If everything is hidden, then also hide the video bar itself. + if (canHideActionBar() && actionBarRule.check(identifier).isBlocked()) return true; - var currentIsActionButton = actionButtonsRule.check(path).isBlocked(); - - if (dislikeRule.check(path).isBlocked()) ActionButton.doNotBlockCounter = 4; - - if (currentIsActionButton && ActionButton.doNotBlockCounter-- > 0) { - if (SettingsEnum.HIDE_SHARE_BUTTON.getBoolean()) { - LogHelper.printDebug(() -> "Hiding share button"); - return true; - } else return false; - } - - if ((currentIsActionButton && ActionButton.doNotBlockCounter <= 0 && actionButtonsRule.isEnabled()) || pathRegister.contains(path)) { - LogHelper.printDebug(() -> "Blocked: " + path); - return true; - } else return false; - } - - static class ActionButton { - public static int doNotBlockCounter = 4; + return pathRegister.contains(path); } } diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index 2ddad19d..5c742833 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -71,12 +71,10 @@ public enum SettingsEnum { VIDEO_ADS_REMOVAL("revanced_video_ads_removal", BOOLEAN, TRUE, true), // Action buttons - HIDE_ACTION_BUTTON("revanced_hide_action_button", BOOLEAN, FALSE), - HIDE_DISLIKE_BUTTON("revanced_hide_dislike_button", BOOLEAN, FALSE), + HIDE_LIKE_DISLIKE_BUTTON("revanced_hide_like_dislike_button", BOOLEAN, FALSE), HIDE_DOWNLOAD_BUTTON("revanced_hide_download_button", BOOLEAN, FALSE), - HIDE_LIKE_BUTTON("revanced_hide_like_button", BOOLEAN, FALSE), HIDE_PLAYLIST_BUTTON("revanced_hide_playlist_button", BOOLEAN, FALSE), - HIDE_SHARE_BUTTON("revanced_hide_share_button", BOOLEAN, FALSE), + HIDE_ACTION_BUTTONS("revanced_hide_action_buttons", BOOLEAN, FALSE), // Layout settings DISABLE_STARTUP_SHORTS_PLAYER("revanced_startup_shorts_player_enabled", BOOLEAN, FALSE), From 587689ed7b0f86b323bd330ebd9d26a1f1f8f31d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 20 Apr 2023 06:43:27 +0000 Subject: [PATCH 30/30] chore(release): 0.103.0-dev.7 [skip ci] # [0.103.0-dev.7](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.6...v0.103.0-dev.7) (2023-04-20) ### Bug Fixes * **youtube/hide-video-action-buttons:** fix 'hide share button' ([#360](https://github.com/revanced/revanced-integrations/issues/360)) ([a2af2c0](https://github.com/revanced/revanced-integrations/commit/a2af2c0c9ffc2f961773bfb8d546aff68c2d1c27)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e329e4e7..30db9fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [0.103.0-dev.7](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.6...v0.103.0-dev.7) (2023-04-20) + + +### Bug Fixes + +* **youtube/hide-video-action-buttons:** fix 'hide share button' ([#360](https://github.com/revanced/revanced-integrations/issues/360)) ([a2af2c0](https://github.com/revanced/revanced-integrations/commit/a2af2c0c9ffc2f961773bfb8d546aff68c2d1c27)) + # [0.103.0-dev.6](https://github.com/revanced/revanced-integrations/compare/v0.103.0-dev.5...v0.103.0-dev.6) (2023-04-19) diff --git a/gradle.properties b/gradle.properties index 780bfd68..c2e39826 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs = -Xmx2048m android.useAndroidX = true -version = 0.103.0-dev.6 +version = 0.103.0-dev.7