From 564a0443b85807cab156c3fafea804c560c20498 Mon Sep 17 00:00:00 2001 From: Aaron Veil <70171475+anddea@users.noreply.github.com> Date: Mon, 27 May 2024 09:47:44 +0300 Subject: [PATCH] feat(YouTube - Overlay buttons): Add `Whitelist` overlay button --- .../overlaybutton/TimeOrderedPlaylist.java | 2 +- .../patches/overlaybutton/Whitelists.java | 55 ++++ .../youtube/patches/utils/PatchStatus.java | 5 + .../patches/video/PlaybackSpeedPatch.java | 14 +- .../youtube/settings/Settings.java | 1 + .../ReVancedSettingsPreference.java | 14 + .../WhitelistedChannelsPreference.java | 164 ++++++++++ .../youtube/shared/VideoInformation.java | 20 ++ .../SegmentPlaybackController.java | 5 + .../youtube/utils/ThemeUtils.java | 8 + .../youtube/whitelist/VideoChannel.java | 21 ++ .../youtube/whitelist/Whitelist.java | 293 ++++++++++++++++++ 12 files changed, 598 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/Whitelists.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/whitelist/VideoChannel.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/whitelist/Whitelist.java diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/TimeOrderedPlaylist.java b/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/TimeOrderedPlaylist.java index 0863644a84..0ac2ebbf2a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/TimeOrderedPlaylist.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/TimeOrderedPlaylist.java @@ -51,4 +51,4 @@ public static void changeVisibilityNegatedImmediate() { if (instance != null) instance.setVisibilityNegatedImmediate(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/Whitelists.java b/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/Whitelists.java new file mode 100644 index 0000000000..e0748a3ae9 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/Whitelists.java @@ -0,0 +1,55 @@ +package app.revanced.integrations.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.integrations.shared.utils.Logger; +import app.revanced.integrations.youtube.settings.Settings; +import app.revanced.integrations.youtube.settings.preference.WhitelistedChannelsPreference; +import app.revanced.integrations.youtube.whitelist.Whitelist; + +@SuppressWarnings("unused") +public class Whitelists extends BottomControlButton { + @Nullable + private static Whitelists instance; + + public Whitelists(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "whitelist_button", + Settings.OVERLAY_BUTTON_WHITELIST, + view -> Whitelist.showWhitelistDialog(view.getContext()), + view -> { + WhitelistedChannelsPreference.showWhitelistedChannelDialog(view.getContext()); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new Whitelists(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java index 6aa69b406f..f7de25c642 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java @@ -17,6 +17,11 @@ public static boolean RememberPlaybackSpeed() { return false; } + public static boolean SponsorBlock() { + // Replace this with true if the SponsorBlock patch succeeds + return false; + } + public static boolean ToolBarComponents() { // Replace this with true if the Toolbar components patch succeeds return false; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/video/PlaybackSpeedPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/video/PlaybackSpeedPatch.java index 8d77887217..f97f907c10 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/video/PlaybackSpeedPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/video/PlaybackSpeedPatch.java @@ -9,6 +9,7 @@ import app.revanced.integrations.youtube.patches.utils.PatchStatus; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.VideoInformation; +import app.revanced.integrations.youtube.whitelist.Whitelist; @SuppressWarnings("unused") public class PlaybackSpeedPatch { @@ -21,13 +22,16 @@ public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNul @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { isLiveStream = newlyLoadedLiveStreamValue; + Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && newlyLoadedLiveStreamValue) return; - Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); + float defaultPlaybackSpeed = Settings.DEFAULT_PLAYBACK_SPEED.get(); + if (Whitelist.isChannelWhitelistedPlaybackSpeed(newlyLoadedChannelId)) + defaultPlaybackSpeed = 1.0f; - VideoInformation.overridePlaybackSpeed(Settings.DEFAULT_PLAYBACK_SPEED.get()); + VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed); } /** @@ -41,9 +45,13 @@ public static float getPlaybackSpeedInShorts(final float playbackSpeed) { if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && isLiveStream) return playbackSpeed; + float defaultPlaybackSpeed = Settings.DEFAULT_PLAYBACK_SPEED.get(); + if (Whitelist.isChannelWhitelistedPlaybackSpeed(VideoInformation.getChannelId())) + defaultPlaybackSpeed = 1.0f; + Logger.printDebug(() -> "getPlaybackSpeedInShorts: " + playbackSpeed); - return Settings.DEFAULT_PLAYBACK_SPEED.get(); + return defaultPlaybackSpeed; } /** diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index f9a877444a..0c744aad14 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -303,6 +303,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE); public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE); public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", TRUE); + public static final BooleanSetting OVERLAY_BUTTON_WHITELIST = new BooleanSetting("revanced_overlay_button_whitelist", FALSE); public static final BooleanSetting OVERLAY_BUTTON_TIME_ORDERED_PLAYLIST = new BooleanSetting("revanced_overlay_button_time_ordered_playlist", FALSE); public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_package_name", "com.deniscerri.ytdl"); public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action", FALSE); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java index 61d876d4fa..80d4024965 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedSettingsPreference.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import app.revanced.integrations.shared.settings.Setting; +import app.revanced.integrations.youtube.patches.utils.PatchStatus; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.utils.ExtendedUtils; @@ -48,6 +49,7 @@ public static void initializeReVancedSettings(@NonNull Activity activity) { SpeedOverlayPreferenceLinks(); QuickActionsPreferenceLinks(); TabletLayoutLinks(); + WhitelistPreferenceLinks(); } /** @@ -200,4 +202,16 @@ private static void SpeedOverlayPreferenceLinks() { Settings.SPEED_OVERLAY_VALUE ); } + + private static void WhitelistPreferenceLinks() { + final boolean enabled = PatchStatus.RememberPlaybackSpeed() || PatchStatus.SponsorBlock(); + final String [] whitelistKey = { Settings.OVERLAY_BUTTON_WHITELIST.key, "revanced_whitelist_settings" }; + + for (String key: whitelistKey) { + final Preference preference = mPreferenceManager.findPreference(key); + if (preference != null) { + preference.setEnabled(enabled); + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java new file mode 100644 index 0000000000..fb73fc5610 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/WhitelistedChannelsPreference.java @@ -0,0 +1,164 @@ +package app.revanced.integrations.youtube.settings.preference; + +import static app.revanced.integrations.shared.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.ArrayList; + +import app.revanced.integrations.shared.utils.Utils; +import app.revanced.integrations.youtube.patches.utils.PatchStatus; +import app.revanced.integrations.youtube.utils.ThemeUtils; +import app.revanced.integrations.youtube.whitelist.VideoChannel; +import app.revanced.integrations.youtube.whitelist.Whitelist; +import app.revanced.integrations.youtube.whitelist.Whitelist.WhitelistType; + +/** + * @noinspection ALL + */ +public class WhitelistedChannelsPreference extends Preference implements Preference.OnPreferenceClickListener { + + private static final WhitelistType whitelistTypePlaybackSpeed = WhitelistType.PLAYBACK_SPEED; + private static final WhitelistType whitelistTypeSponsorBlock = WhitelistType.SPONSOR_BLOCK; + private static final boolean playbackSpeedIncluded = PatchStatus.RememberPlaybackSpeed(); + private static final boolean sponsorBlockIncluded = PatchStatus.SponsorBlock(); + private static String[] mEntries; + private static WhitelistType[] mEntryValues; + + static { + final int entrySize = BooleanUtils.toInteger(playbackSpeedIncluded) + + BooleanUtils.toInteger(sponsorBlockIncluded); + + if (entrySize != 0 && mEntries == null && mEntryValues == null) { + mEntries = new String[entrySize]; + mEntryValues = new WhitelistType[entrySize]; + + int index = 0; + if (playbackSpeedIncluded) { + mEntries[index] = " " + whitelistTypePlaybackSpeed.getFriendlyName() + " "; + mEntryValues[index] = whitelistTypePlaybackSpeed; + index++; + } + if (sponsorBlockIncluded) { + mEntries[index] = " " + whitelistTypeSponsorBlock.getFriendlyName() + " "; + mEntryValues[index] = whitelistTypeSponsorBlock; + } + } + } + + private void init() { + setOnPreferenceClickListener(this); + } + + public WhitelistedChannelsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + public WhitelistedChannelsPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + public WhitelistedChannelsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + public WhitelistedChannelsPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + showWhitelistedChannelDialog(getContext()); + + return true; + } + + public static void showWhitelistedChannelDialog(Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(str("revanced_whitelist_settings_title")); + builder.setItems(mEntries, (dialog, which) -> showWhitelistedChannelDialog(context, mEntryValues[which])); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + private static void showWhitelistedChannelDialog(Context context, WhitelistType whitelistType) { + final ArrayList mEntries = Whitelist.getWhitelistedChannels(whitelistType); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(whitelistType.getFriendlyName()); + + if (mEntries.isEmpty()) { + TextView emptyView = new TextView(context); + emptyView.setText(str("revanced_whitelist_empty")); + emptyView.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_START); + emptyView.setTextSize(16); + emptyView.setPadding(60, 40, 60, 0); + builder.setView(emptyView); + } else { + LinearLayout entriesContainer = new LinearLayout(context); + entriesContainer.setOrientation(LinearLayout.VERTICAL); + for (final VideoChannel entry : mEntries) { + String author = entry.getChannelName(); + View entryView = getEntryView(context, author, v -> { + new AlertDialog.Builder(context) + .setMessage(str("revanced_whitelist_remove_dialog_message", author, whitelistType.getFriendlyName())) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Whitelist.removeFromWhitelist(whitelistType, entry.getChannelId()); + entriesContainer.removeView(entriesContainer.findViewWithTag(author)); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + }); + entryView.setTag(author); + entriesContainer.addView(entryView); + } + builder.setView(entriesContainer); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + + private static View getEntryView(Context context, CharSequence entry, View.OnClickListener onDeleteClickListener) { + LinearLayout.LayoutParams entryContainerParams = new LinearLayout.LayoutParams( + new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + entryContainerParams.setMargins(60, 40, 60, 0); + + LinearLayout.LayoutParams entryLabelLayoutParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + entryLabelLayoutParams.gravity = Gravity.CENTER; + + LinearLayout entryContainer = new LinearLayout(context); + entryContainer.setOrientation(LinearLayout.HORIZONTAL); + entryContainer.setLayoutParams(entryContainerParams); + + TextView entryLabel = new TextView(context); + entryLabel.setText(entry); + entryLabel.setLayoutParams(entryLabelLayoutParams); + entryLabel.setTextSize(16); + entryLabel.setOnClickListener(onDeleteClickListener); + + ImageButton deleteButton = new ImageButton(context); + deleteButton.setImageDrawable(ThemeUtils.getTrashButtonDrawable()); + deleteButton.setOnClickListener(onDeleteClickListener); + deleteButton.setBackground(null); + + entryContainer.addView(entryLabel); + entryContainer.addView(deleteButton); + return entryContainer; + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/VideoInformation.java b/app/src/main/java/app/revanced/integrations/youtube/shared/VideoInformation.java index 2a1615e567..63f08654a1 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/VideoInformation.java @@ -189,6 +189,26 @@ public static String getVideoId() { return videoId; } + /** + * Channel Name of the last video opened. Includes Shorts. + * + * @return The channel name of the video. + */ + @NonNull + public static String getChannelName() { + return channelName; + } + + /** + * ChannelId of the last video opened. Includes Shorts. + * + * @return The channel id of the video. + */ + @NonNull + public static String getChannelId() { + return channelId; + } + public static boolean getLiveStreamState() { return videoIsLiveStream; } diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/SegmentPlaybackController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/SegmentPlaybackController.java index dd2254fccc..df4e93bb96 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/SegmentPlaybackController.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/SegmentPlaybackController.java @@ -30,6 +30,7 @@ import app.revanced.integrations.youtube.sponsorblock.objects.SponsorSegment; import app.revanced.integrations.youtube.sponsorblock.requests.SBRequester; import app.revanced.integrations.youtube.sponsorblock.ui.SponsorBlockViewController; +import app.revanced.integrations.youtube.whitelist.Whitelist; /** * Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video. @@ -228,6 +229,10 @@ public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNul videoLength = newlyLoadedVideoLength; Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); + if (Whitelist.isChannelWhitelistedSponsorBlock(newlyLoadedChannelId)) { + return; + } + Utils.runOnBackgroundThread(() -> { try { executeDownloadSegments(newlyLoadedVideoId); diff --git a/app/src/main/java/app/revanced/integrations/youtube/utils/ThemeUtils.java b/app/src/main/java/app/revanced/integrations/youtube/utils/ThemeUtils.java index 832956431e..b15e365d86 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/utils/ThemeUtils.java +++ b/app/src/main/java/app/revanced/integrations/youtube/utils/ThemeUtils.java @@ -37,6 +37,14 @@ public static Drawable getBackButtonDrawable() { return getDrawable(drawableName); } + public static Drawable getTrashButtonDrawable() { + final String drawableName = isDarkTheme() + ? "yt_outline_trash_can_white_24" + : "yt_outline_trash_can_black_24"; + + return getDrawable(drawableName); + } + public static int getTextColor() { final String colorName = isDarkTheme() ? "yt_white1" diff --git a/app/src/main/java/app/revanced/integrations/youtube/whitelist/VideoChannel.java b/app/src/main/java/app/revanced/integrations/youtube/whitelist/VideoChannel.java new file mode 100644 index 0000000000..f5879cb096 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/whitelist/VideoChannel.java @@ -0,0 +1,21 @@ +package app.revanced.integrations.youtube.whitelist; + +import java.io.Serializable; + +public final class VideoChannel implements Serializable { + private String channelName; + private String channelId; + + public VideoChannel(String channelName, String channelId) { + this.channelName = channelName; + this.channelId = channelId; + } + + public String getChannelName() { + return channelName; + } + + public String getChannelId() { + return channelId; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/whitelist/Whitelist.java b/app/src/main/java/app/revanced/integrations/youtube/whitelist/Whitelist.java new file mode 100644 index 0000000000..c7685b62c4 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/whitelist/Whitelist.java @@ -0,0 +1,293 @@ +package app.revanced.integrations.youtube.whitelist; + +import static app.revanced.integrations.shared.utils.StringRef.str; +import static app.revanced.integrations.shared.utils.Utils.showToastShort; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.widget.Button; + +import androidx.annotation.NonNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import app.revanced.integrations.shared.utils.Logger; +import app.revanced.integrations.shared.utils.ResourceUtils; +import app.revanced.integrations.shared.utils.Utils; +import app.revanced.integrations.youtube.patches.utils.PatchStatus; +import app.revanced.integrations.youtube.shared.VideoInformation; +import app.revanced.integrations.youtube.utils.ThemeUtils; + +public class Whitelist { + private static final String ZERO_WIDTH_SPACE_CHARACTER = "\u200B"; + private static final Map> whitelistMap = parseWhitelist(); + + private static final WhitelistType whitelistTypePlaybackSpeed = WhitelistType.PLAYBACK_SPEED; + private static final WhitelistType whitelistTypeSponsorBlock = WhitelistType.SPONSOR_BLOCK; + private static final Drawable playbackSpeedDrawable = + ResourceUtils.getDrawable("yt_outline_play_arrow_half_circle_black_24"); + private static final Drawable sponsorBlockDrawable = + ResourceUtils.getDrawable("revanced_sb_logo"); + private static final String whitelistIncluded = str("revanced_whitelist_included"); + private static final String whitelistExcluded = str("revanced_whitelist_excluded"); + + public static boolean isChannelWhitelistedSponsorBlock(String channelId) { + return isWhitelisted(whitelistTypeSponsorBlock, channelId); + } + + public static boolean isChannelWhitelistedPlaybackSpeed(String channelId) { + return isWhitelisted(whitelistTypePlaybackSpeed, channelId); + } + + public static void showWhitelistDialog(Context context) { + final String channelId = VideoInformation.getChannelId(); + final String channelName = VideoInformation.getChannelName(); + + if (channelId.isEmpty() || channelName.isEmpty()) { + Utils.showToastShort(str("revanced_whitelist_failure_generic")); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(channelName); + + StringBuilder sb = new StringBuilder("\n"); + + if (PatchStatus.RememberPlaybackSpeed()) { + appendStringBuilder(sb, whitelistTypePlaybackSpeed, channelId, false); + builder.setNeutralButton(ZERO_WIDTH_SPACE_CHARACTER, + (dialog, id) -> whitelistListener( + whitelistTypePlaybackSpeed, + channelId, + channelName + ) + ); + } + + if (PatchStatus.SponsorBlock()) { + appendStringBuilder(sb, whitelistTypeSponsorBlock, channelId, true); + builder.setPositiveButton(ZERO_WIDTH_SPACE_CHARACTER, + (dialog, id) -> whitelistListener( + whitelistTypeSponsorBlock, + channelId, + channelName + ) + ); + } + + builder.setMessage(sb.toString()); + + AlertDialog dialog = builder.show(); + + final ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getTextColor(), PorterDuff.Mode.SRC_ATOP); + Button sponsorBlockButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + Button playbackSpeedButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (sponsorBlockButton != null) { + sponsorBlockDrawable.setColorFilter(cf); + sponsorBlockButton.setCompoundDrawablesWithIntrinsicBounds(null, null, sponsorBlockDrawable, null); + } + if (playbackSpeedButton != null) { + playbackSpeedDrawable.setColorFilter(cf); + playbackSpeedButton.setCompoundDrawablesWithIntrinsicBounds(playbackSpeedDrawable, null, null, null); + } + } + + private static void appendStringBuilder(StringBuilder sb, WhitelistType whitelistType, + String channelId, boolean eol) { + final String status = isWhitelisted(whitelistType, channelId) + ? whitelistIncluded + : whitelistExcluded; + sb.append(whitelistType.getFriendlyName()); + sb.append(":\n"); + sb.append(status); + sb.append("\n"); + if (!eol) sb.append("\n"); + } + + private static void whitelistListener(WhitelistType whitelistType, String channelId, String channelName) { + try { + if (isWhitelisted(whitelistType, channelId)) { + removeFromWhitelist(whitelistType, channelId); + } else { + addToWhitelist(whitelistType, channelId, channelName); + } + } catch (Exception ex) { + Logger.printException(() -> "whitelistListener failure", ex); + } + } + + /** @noinspection unchecked*/ + private static Map> parseWhitelist() { + WhitelistType[] whitelistTypes = WhitelistType.values(); + Map> whitelistMap = new EnumMap<>(WhitelistType.class); + + for (WhitelistType whitelistType : whitelistTypes) { + SharedPreferences preferences = getPreferences(whitelistType.getPreferencesName()); + String serializedChannels = preferences.getString("channels", null); + if (serializedChannels == null) { + whitelistMap.put(whitelistType, new ArrayList<>()); + continue; + } + try { + Object channelsObject = deserialize(serializedChannels); + ArrayList deserializedChannels = (ArrayList) channelsObject; + whitelistMap.put(whitelistType, deserializedChannels); + } catch (Exception ex) { + Logger.printException(() -> "parseWhitelist failure", ex); + } + } + return whitelistMap; + } + + private static boolean isWhitelisted(WhitelistType whitelistType, String channelId) { + for (VideoChannel channel : getWhitelistedChannels(whitelistType)) { + if (channel.getChannelId().equals(channelId)) { + return true; + } + } + return false; + } + + private static void addToWhitelist(WhitelistType whitelistType, String channelId, String channelName) { + final VideoChannel channel = new VideoChannel(channelName, channelId); + ArrayList whitelisted = getWhitelistedChannels(whitelistType); + for (VideoChannel whitelistedChannel : whitelisted) { + if (whitelistedChannel.getChannelId().equals(channel.getChannelId())) + return; + } + whitelisted.add(channel); + String friendlyName = whitelistType.getFriendlyName(); + if (updateWhitelist(whitelistType, whitelisted)) { + showToastShort(str("revanced_whitelist_added", channelName, friendlyName)); + } else { + showToastShort(str("revanced_whitelist_add_failed", channelName, friendlyName)); + } + } + + public static void removeFromWhitelist(WhitelistType whitelistType, String channelId) { + ArrayList whitelisted = getWhitelistedChannels(whitelistType); + Iterator iterator = whitelisted.iterator(); + String channelName = ""; + while (iterator.hasNext()) { + VideoChannel channel = iterator.next(); + if (channel.getChannelId().equals(channelId)) { + channelName = channel.getChannelName(); + iterator.remove(); + break; + } + } + String friendlyName = whitelistType.getFriendlyName(); + if (updateWhitelist(whitelistType, whitelisted)) { + showToastShort(str("revanced_whitelist_removed", channelName, friendlyName)); + } else { + showToastShort(str("revanced_whitelist_remove_failed", channelName, friendlyName)); + } + } + + private static boolean updateWhitelist(WhitelistType whitelistType, ArrayList channels) { + SharedPreferences.Editor editor = getPreferences(whitelistType.getPreferencesName()).edit(); + + final String channelName = serialize(channels); + if (channelName != null && !channelName.isEmpty()) { + editor.putString("channels", channelName); + editor.apply(); + return true; + } + return false; + } + + public static ArrayList getWhitelistedChannels(WhitelistType whitelistType) { + return whitelistMap.get(whitelistType); + } + + private static SharedPreferences getPreferences(@NonNull String prefName) { + final Context context = Utils.getContext(); + return context.getSharedPreferences(prefName, Context.MODE_PRIVATE); + } + + private static String serialize(Serializable obj) { + try { + if (obj != null) { + ByteArrayOutputStream serialObj = new ByteArrayOutputStream(); + Deflater def = new Deflater(Deflater.BEST_COMPRESSION); + ObjectOutputStream objStream = + new ObjectOutputStream(new DeflaterOutputStream(serialObj, def)); + objStream.writeObject(obj); + objStream.close(); + return encodeBytes(serialObj.toByteArray()); + } + } catch (IOException ex) { + Logger.printException(() -> "Serialization error: " + ex.getMessage(), ex); + } + return null; + } + + private static Object deserialize(@NonNull String str) { + try { + final ByteArrayInputStream serialObj = new ByteArrayInputStream(decodeBytes(str)); + final ObjectInputStream objStream = new ObjectInputStream(new InflaterInputStream(serialObj)); + return objStream.readObject(); + } catch (ClassNotFoundException | IOException ex) { + Logger.printException(() -> "Deserialization error: " + ex.getMessage(), ex); + } + return null; + } + + private static String encodeBytes(byte[] bytes) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Base64.getEncoder().encodeToString(bytes); + } else { + return new String(bytes, StandardCharsets.UTF_8); + } + } + + private static byte[] decodeBytes(String str) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Base64.getDecoder().decode(str.getBytes(StandardCharsets.UTF_8)); + } else { + return str.getBytes(StandardCharsets.UTF_8); + } + } + + public enum WhitelistType { + PLAYBACK_SPEED(), + SPONSOR_BLOCK(); + + private final String friendlyName; + private final String preferencesName; + + WhitelistType() { + String name = name().toLowerCase(); + this.friendlyName = str("revanced_whitelist_" + name); + this.preferencesName = "whitelist_" + name; + } + + public String getFriendlyName() { + return friendlyName; + } + + public String getPreferencesName() { + return preferencesName; + } + } +}