From ac2e3937ced4cd10c009b0e2b8aa4d81e2bf678c Mon Sep 17 00:00:00 2001 From: jakweg Date: Mon, 24 Aug 2020 17:47:57 +0200 Subject: [PATCH] Initial commit --- integrations/AndroidManifest.xml | 12 + integrations/java/pl/jakubweg/Helper.java | 19 + .../java/pl/jakubweg/InjectedPlugin.java | 98 +++ .../pl/jakubweg/NewSegmentHelperLayout.java | 139 ++++ .../java/pl/jakubweg/PlayerController.java | 489 +++++++++++++ .../java/pl/jakubweg/SkipSegmentView.java | 95 +++ .../SponsorBlockPreferenceFragment.java | 249 +++++++ .../pl/jakubweg/SponsorBlockSettings.java | 215 ++++++ .../java/pl/jakubweg/SponsorBlockUtils.java | 648 ++++++++++++++++++ .../java/pl/jakubweg/SponsorSegment.java | 49 ++ integrations/res/drawable/ic_sb_adjust.xml | 10 + integrations/res/drawable/ic_sb_compare.xml | 10 + integrations/res/drawable/ic_sb_edit.xml | 10 + integrations/res/drawable/ic_sb_logo.xml | 19 + integrations/res/drawable/ic_sb_publish.xml | 10 + integrations/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes integrations/res/values/strings.xml | 180 +++++ 17 files changed, 2252 insertions(+) create mode 100644 integrations/AndroidManifest.xml create mode 100644 integrations/java/pl/jakubweg/Helper.java create mode 100644 integrations/java/pl/jakubweg/InjectedPlugin.java create mode 100644 integrations/java/pl/jakubweg/NewSegmentHelperLayout.java create mode 100644 integrations/java/pl/jakubweg/PlayerController.java create mode 100644 integrations/java/pl/jakubweg/SkipSegmentView.java create mode 100644 integrations/java/pl/jakubweg/SponsorBlockPreferenceFragment.java create mode 100644 integrations/java/pl/jakubweg/SponsorBlockSettings.java create mode 100644 integrations/java/pl/jakubweg/SponsorBlockUtils.java create mode 100644 integrations/java/pl/jakubweg/SponsorSegment.java create mode 100644 integrations/res/drawable/ic_sb_adjust.xml create mode 100644 integrations/res/drawable/ic_sb_compare.xml create mode 100644 integrations/res/drawable/ic_sb_edit.xml create mode 100644 integrations/res/drawable/ic_sb_logo.xml create mode 100644 integrations/res/drawable/ic_sb_publish.xml create mode 100644 integrations/res/mipmap-hdpi/ic_launcher.png create mode 100644 integrations/res/values/strings.xml diff --git a/integrations/AndroidManifest.xml b/integrations/AndroidManifest.xml new file mode 100644 index 000000000..80c110cdc --- /dev/null +++ b/integrations/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/integrations/java/pl/jakubweg/Helper.java b/integrations/java/pl/jakubweg/Helper.java new file mode 100644 index 000000000..6ded2a5ff --- /dev/null +++ b/integrations/java/pl/jakubweg/Helper.java @@ -0,0 +1,19 @@ +package pl.jakubweg; + +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +public class Helper { + + public static String getStringByName(Context context, String name) { + try { + Resources res = context.getResources(); + return res.getString(res.getIdentifier(name, "string", context.getPackageName())); + } catch (Throwable exception) { + Log.e("XGlobals", "Resource not found.", exception); + return ""; + } + } + +} diff --git a/integrations/java/pl/jakubweg/InjectedPlugin.java b/integrations/java/pl/jakubweg/InjectedPlugin.java new file mode 100644 index 000000000..021038f68 --- /dev/null +++ b/integrations/java/pl/jakubweg/InjectedPlugin.java @@ -0,0 +1,98 @@ +package pl.jakubweg; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.lang.reflect.Field; + +// invoke-static {p0}, Lpl/jakubweg/InjectedPlugin;->inject(Landroid/content/Context;)V +// invoke-static {}, Lpl/jakubweg/InjectedPlugin;->printSomething()V +// InlineTimeBar +public class InjectedPlugin { + + private static final String TAG = "jakubweg.InjectedPlugin"; + + public static void printSomething() { + Log.d(TAG, "printSomething called"); + } + + public static void printObject(Object o, int recursive) { + if (o == null) + Log.d(TAG, "Printed object is null"); + else { + Log.d(TAG, "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())) + Log.d(TAG, "Field: " + field.toString() + " has value " + value); + } catch (Exception e) { + Log.d(TAG, "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(); + Log.d(TAG, "Printing stack trace:"); + for (StackTraceElement element : stackTrace) { + Log.d(TAG, 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) { + Log.i(TAG, spacesStr + "Null view"); + return; + } + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + Log.i(TAG, spacesStr + "View group: " + view); + int childCount = group.getChildCount(); + Log.i(TAG, spacesStr + "Children count: " + childCount); + for (int i = 0; i < childCount; i++) { + printViewStack(group.getChildAt(i), spaces + 1); + } + } else { + Log.i(TAG, spacesStr + "Normal view: " + view); + } + } + +} + + diff --git a/integrations/java/pl/jakubweg/NewSegmentHelperLayout.java b/integrations/java/pl/jakubweg/NewSegmentHelperLayout.java new file mode 100644 index 000000000..5c0ac4c31 --- /dev/null +++ b/integrations/java/pl/jakubweg/NewSegmentHelperLayout.java @@ -0,0 +1,139 @@ +package pl.jakubweg; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import java.lang.ref.WeakReference; + +public class NewSegmentHelperLayout extends LinearLayout implements View.OnClickListener { + private static final int rewindBtnId = 1235; + private static final int forwardBtnId = 1236; + private static final int publishBtnId = 1237; + private static final int hideBtnId = 1238; + private static final int markLocationBtnId = 1239; + private static final int previewBtnId = 1240; + private static final int editByHandBtnId = 1241; + private static WeakReference INSTANCE = new WeakReference<>(null); + private static boolean isShown = false; + private final int padding; + private final int iconSize; + private final int rippleEffectId; + private final String packageName; + + @SuppressLint({"DefaultLocale", "SetTextI18n"}) + public NewSegmentHelperLayout(Context context) { + super(context); + INSTANCE = new WeakReference<>(this); + isShown = false; + setVisibility(GONE); + + packageName = context.getPackageName(); + padding = (int) SkipSegmentView.convertDpToPixel(4f, context); + iconSize = (int) SkipSegmentView.convertDpToPixel(40f, context); + + TypedValue rippleEffect = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true); + rippleEffectId = rippleEffect.resourceId; + + + setOrientation(VERTICAL); + @SuppressLint("RtlHardcoded") + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.START | Gravity.LEFT | Gravity.CENTER_VERTICAL + ); + this.setBackgroundColor(0x66000000); + this.bringToFront(); + this.setLayoutParams(layoutParams); + this.setPadding(padding, padding, padding, padding); + + final LinearLayout topLayout = new LinearLayout(context); + final LinearLayout bottomLayout = new LinearLayout(context); + topLayout.setOrientation(HORIZONTAL); + bottomLayout.setOrientation(HORIZONTAL); + this.addView(topLayout); + this.addView(bottomLayout); + + topLayout.addView(createTextViewBtn(rewindBtnId, "player_fast_rewind")); + topLayout.addView(createTextViewBtn(forwardBtnId, "player_fast_forward")); + topLayout.addView(createTextViewBtn(markLocationBtnId, "ic_sb_adjust")); + bottomLayout.addView(createTextViewBtn(previewBtnId, "ic_sb_compare")); + bottomLayout.addView(createTextViewBtn(editByHandBtnId, "ic_sb_edit")); + bottomLayout.addView(createTextViewBtn(publishBtnId, "ic_sb_publish")); +// bottomLayout.addView(createTextViewBtn(hideBtnId,"btn_close_light")); + } + + public static void show() { + if (isShown) return; + isShown = true; + NewSegmentHelperLayout i = INSTANCE.get(); + if (i == null) return; + i.setVisibility(VISIBLE); + i.bringToFront(); + i.requestLayout(); + i.invalidate(); + } + + public static void hide() { + if (!isShown) return; + isShown = false; + NewSegmentHelperLayout i = INSTANCE.get(); + if (i != null) + i.setVisibility(GONE); + } + + public static void toggle() { + if (isShown) hide(); + else show(); + } + + private View createTextViewBtn(int id, String drawableName) { + int drawableId = getResources().getIdentifier(drawableName, "drawable", packageName); + final ImageView view = new ImageView(getContext()); + view.setPadding(padding, padding, padding, padding); + view.setLayoutParams(new LayoutParams(iconSize, iconSize, 1)); + view.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + view.setImageResource(drawableId); + view.setId(id); + view.setClickable(true); + view.setFocusable(true); + view.setBackgroundResource(rippleEffectId); + view.setOnClickListener(this); + return view; + } + + + @Override + public void onClick(View v) { + switch (v.getId()) { + case forwardBtnId: + PlayerController.skipRelativeMilliseconds(SponsorBlockSettings.adjustNewSegmentMillis); + break; + case rewindBtnId: + PlayerController.skipRelativeMilliseconds(-SponsorBlockSettings.adjustNewSegmentMillis); + break; + case markLocationBtnId: + SponsorBlockUtils.onMarkLocationClicked(getContext()); + break; + case publishBtnId: + SponsorBlockUtils.onPublishClicked(getContext()); + break; + case previewBtnId: + SponsorBlockUtils.onPreviewClicked(getContext()); + break; + case editByHandBtnId: + SponsorBlockUtils.onEditByHandClicked(getContext()); + break; + case hideBtnId: + hide(); + break; + } + } +} diff --git a/integrations/java/pl/jakubweg/PlayerController.java b/integrations/java/pl/jakubweg/PlayerController.java new file mode 100644 index 000000000..db7bc4303 --- /dev/null +++ b/integrations/java/pl/jakubweg/PlayerController.java @@ -0,0 +1,489 @@ +package pl.jakubweg; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Timer; +import java.util.TimerTask; + +@SuppressLint({"LongLogTag"}) +public class PlayerController { + public static final String TAG = "jakubweg.PlayerController"; + public static final boolean VERBOSE = false; + @SuppressWarnings("PointlessBooleanExpression") + public static final boolean VERBOSE_DRAW_OPTIONS = false && VERBOSE; + + private static final Timer sponsorTimer = new Timer("sponsor-skip-timer"); + public static WeakReference playerActivity = new WeakReference<>(null); + public static SponsorSegment[] sponsorSegmentsOfCurrentVideo; + private static WeakReference currentPlayerController = new WeakReference<>(null); + private static Method setMillisecondMethod; + private static long allowNextSkipRequestTime = 0L; + private static String currentVideoId; + private static long currentVideoLength = 1L; + private static long lastKnownVideoTime = -1L; + private static final Runnable findAndSkipSegmentRunnable = new Runnable() { + @Override + public void run() { +// Log.d(TAG, "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) { + if (videoId == null) { + Log.d(TAG, "setCurrentVideoId: videoId is null"); + return; + } + + if (!SponsorBlockSettings.isSponsorBlockEnabled) { + currentVideoId = null; + return; + } + + if (Looper.myLooper() != Looper.getMainLooper()) // check if thread is not main + return; + + if (videoId.equals(currentVideoId)) + return; + + currentVideoId = videoId; + sponsorSegmentsOfCurrentVideo = null; + if (VERBOSE) + Log.d(TAG, "setCurrentVideoId: videoId=" + videoId); + + sponsorTimer.schedule(new TimerTask() { + @Override + public void run() { + executeDownloadSegments(currentVideoId, false); + } + }, 0); + } + + /** + * Called when creating some kind of youtube internal player controlled, every time when new video starts to play + */ + public static void onCreate(final Object o) { +// "Plugin.printStackTrace(); + + if (o == null) { + Log.e(TAG, "onCreate called with null object"); + return; + } + + if (VERBOSE) + Log.i(TAG, String.format("onCreate called with object %s on thread %s", o.toString(), Thread.currentThread().toString())); + + try { + setMillisecondMethod = o.getClass().getMethod("a", Long.TYPE); + setMillisecondMethod.setAccessible(true); + + lastKnownVideoTime = 0; + currentVideoLength = 1; + currentPlayerController = new WeakReference<>(o); + + SkipSegmentView.hide(); + NewSegmentHelperLayout.hide(); + + // add image button when starting new video + Activity activity = playerActivity.get(); + if (activity != null) + SponsorBlockUtils.addImageButton(activity, 5); + } catch (Exception e) { + Log.e(TAG, "Exception while initializing skip method", e); + } + } + + public static void executeDownloadSegments(String videoId, boolean ignoreCache) { + SponsorSegment[] segments = SponsorBlockUtils.getSegmentsForVideo(videoId, ignoreCache); + Arrays.sort(segments); + + if (VERBOSE) + for (SponsorSegment segment : segments) { + Log.v(TAG, "Detected segment: " + segment.toString()); + } + + sponsorSegmentsOfCurrentVideo = segments; +// new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable); + } + + /** + * Works in 14.x, waits some time of object to me filled with data, + * No longer used, i've found another way to get faster videoId + */ + @Deprecated + public static void asyncGetVideoLinkFromObject(final Object o) { + // code no longer used + + // if (currentVideoLink != null) { +// if (VERBOSE) +// Log.w(TAG, "asyncGetVideoLinkFromObject: currentVideoLink != null probably share button was clicked"); +// return; +// } +// +// new Thread(new Runnable() { +// @Override +// public void run() { +// try { +// // It used to be "b" in 14.x version, it's "a" in 15.x +// Field b = o.getClass().getDeclaredField("b"); +// +// int attempts = 0; +// String videoUrl = null; +// while (true) { +// Object objLink = b.get(o); +// if (objLink == null) { +// if (VERBOSE) +// Log.e(TAG, "asyncGetVideoLinkFromObject: objLink is null"); +// } else { +// videoUrl = objLink.toString(); +// if (videoUrl.isEmpty()) +// videoUrl = null; +// } +// +// if (videoUrl != null) +// break; +// +// if (attempts++ > 5) { +// Log.w(TAG, "asyncGetVideoLinkFromObject: attempts++ > 5"); +// return; +// } +// Thread.sleep(50); +// } +// +// if (currentVideoLink == null) { +// currentVideoLink = videoUrl; +// if (VERBOSE) +// Log.d(TAG, "asyncGetVideoLinkFromObject: link set to " + videoUrl); +// +// executeDownloadSegments(substringVideoIdFromLink(videoUrl), false); +// } +// +// } catch (Exception e) { +// Log.e(TAG, "Cannot get link from object", e); +// } +// } +// }).start(); +// +// Activity activity = playerActivity.get(); +// if (activity != null) +// SponsorBlockUtils.addImageButton(activity); + } + + /** + * Called when it's time to update the UI with new second, about once per second, only when playing, also in background + */ + public static void setCurrentVideoTime(long millis) { + if (VERBOSE) + Log.v(TAG, "setCurrentVideoTime: current video time: " + millis); + if (!SponsorBlockSettings.isSponsorBlockEnabled) return; + lastKnownVideoTime = millis; + if (millis <= 0) return; + //findAndSkipSegment(false); + + 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) { + if (VERBOSE) + Log.d(TAG, "Scheduling skipSponsorTask"); + skipSponsorTask = new TimerTask() { + @Override + public void run() { + skipSponsorTask = null; + lastKnownVideoTime = segment.start + 1; + new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable); + } + }; + sponsorTimer.schedule(skipSponsorTask, segment.start - millis); + } else { + if (VERBOSE) + Log.d(TAG, "skipSponsorTask is already scheduled..."); + } + + break; + } + + if (segment.end < millis) + continue; + + // we are in the segment! + if (segment.category.behaviour.skip) { + sendViewRequestAsync(millis, segment); + skipSegment(segment, false); + break; + } else { + SkipSegmentView.show(); + return; + } + } + SkipSegmentView.hide(); + } + + private static void sendViewRequestAsync(final long millis, final SponsorSegment segment) { + new Thread(new Runnable() { + @Override + public void run() { + if (SponsorBlockSettings.countSkips && + segment.category != SponsorBlockSettings.SegmentInfo.Preview && + millis - segment.start < 2000) { + // Only skips from the start should count as a view + SponsorBlockUtils.sendViewCountRequest(segment); + } + } + }).start(); + } + + /** + * Called very high frequency (once every about 100ms), also in background. It sometimes triggers when a video is paused (couple times in the row with the same value) + */ + public static void setCurrentVideoTimeHighPrecision(final long millis) { + if (lastKnownVideoTime > 0) + lastKnownVideoTime = millis; + else + setCurrentVideoTime(millis); + } + + public static long getLastKnownVideoTime() { + return lastKnownVideoTime; + } + + /** + * Called before onDraw method on time bar object, sets video length in millis + */ + public static void setVideoLength(final long length) { + if (VERBOSE_DRAW_OPTIONS) + Log.d(TAG, "setVideoLength: length=" + length); + currentVideoLength = length; + } + + + public static void setSponsorBarAbsoluteLeft(final Rect rect) { + setSponsorBarAbsoluteLeft(rect.left); + } + + public static void setSponsorBarAbsoluteLeft(final float left) { + if (VERBOSE_DRAW_OPTIONS) + Log.d(TAG, String.format("setSponsorBarLeft: left=%.2f", left)); + + sponsorBarLeft = left; + } + + public static void setSponsorBarAbsoluteRight(final Rect rect) { + setSponsorBarAbsoluteRight(rect.right); + } + + public static void setSponsorBarAbsoluteRight(final float right) { + if (VERBOSE_DRAW_OPTIONS) + Log.d(TAG, String.format("setSponsorBarRight: right=%.2f", right)); + + sponsorBarRight = right; + } + + public static void setSponsorBarThickness(final int thickness) { + setSponsorBarThickness((float) thickness); + } + + public static void setSponsorBarThickness(final float thickness) { + if (VERBOSE_DRAW_OPTIONS) + Log.d(TAG, String.format("setSponsorBarThickness: thickness=%.2f", thickness)); + + sponsorBarThickness = thickness; + } + + public static void onSkipSponsorClicked() { + if (VERBOSE) + Log.d(TAG, "Skip segment clicked"); + findAndSkipSegment(true); + } + + + public static void addSkipSponsorView15(final View view) { + playerActivity = new WeakReference<>((Activity) view.getContext()); + if (VERBOSE) + Log.d(TAG, "addSkipSponsorView15: view=" + view.toString()); + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + final ViewGroup viewGroup = (ViewGroup) ((ViewGroup) view).getChildAt(2); + Activity context = ((Activity) viewGroup.getContext()); + viewGroup.addView(new SkipSegmentView(context)); + viewGroup.addView(new NewSegmentHelperLayout(context)); + SponsorBlockUtils.addImageButton(context, 40); + } + }, 500); + } + + public static void addSkipSponsorView14(final View view) { + playerActivity = new WeakReference<>((Activity) view.getContext()); + if (VERBOSE) + Log.d(TAG, "addSkipSponsorView14: view=" + view.toString()); + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + final ViewGroup viewGroup = (ViewGroup) view.getParent(); + Activity activity = (Activity) viewGroup.getContext(); + viewGroup.addView(new SkipSegmentView(activity)); + viewGroup.addView(new NewSegmentHelperLayout(activity)); + + // add image button when creating new activity + SponsorBlockUtils.addImageButton(activity, 5); + +// InjectedPlugin.printViewStack(viewGroup, 0); + +// SponsorBlockUtils.addImageButton(activity); + } + }, 500); + } + + + /** + * Called when it's time to draw time bar + */ + public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { + 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) currentVideoLength * (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); + } + } + + // 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 void 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) { + if (VERBOSE) + Log.w(TAG, "skipToMillisecond: to fast, slow down, because you'll fail"); + return; + } + allowNextSkipRequestTime = now + 100; + + if (setMillisecondMethod == null) { + Log.e(TAG, "setMillisecondMethod is null"); + return; + } + + + final Object currentObj = currentPlayerController.get(); + if (currentObj == null) { + Log.e(TAG, "currentObj is null (might have been collected by GC)"); + return; + } + + + if (VERBOSE) + Log.d(TAG, String.format("Requesting skip to millis=%d on thread %s", millisecond, Thread.currentThread().toString())); + + final long finalMillisecond = millisecond; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + try { + if (VERBOSE) + Log.i(TAG, "Skipping to millis=" + finalMillisecond); + lastKnownVideoTime = finalMillisecond; + setMillisecondMethod.invoke(currentObj, finalMillisecond); + } catch (Exception e) { + Log.e(TAG, "Cannot skip to millisecond", e); + } + } + }); + } + + + private static void findAndSkipSegment(boolean wasClicked) { + 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 || wasClicked)) + return; + + sendViewRequestAsync(millis, segment); + skipSegment(segment, wasClicked); + break; + } + + SkipSegmentView.hide(); + } + + private static void skipSegment(SponsorSegment segment, boolean wasClicked) { +// if (lastSkippedSegment == segment) return; +// lastSkippedSegment = segment; + if (VERBOSE) + Log.d(TAG, "Skipping segment: " + segment.toString()); + + if (SponsorBlockSettings.showToastWhenSkippedAutomatically && !wasClicked) + SkipSegmentView.notifySkipped(segment); + + skipToMillisecond(segment.end + 2); + SkipSegmentView.hide(); + if (segment.category == SponsorBlockSettings.SegmentInfo.Preview) { + SponsorSegment[] newSegments = new SponsorSegment[sponsorSegmentsOfCurrentVideo.length - 1]; + int i = 0; + for (SponsorSegment sponsorSegment : sponsorSegmentsOfCurrentVideo) { + if (sponsorSegment != segment) + newSegments[i++] = sponsorSegment; + } + sponsorSegmentsOfCurrentVideo = newSegments; + } + } +} diff --git a/integrations/java/pl/jakubweg/SkipSegmentView.java b/integrations/java/pl/jakubweg/SkipSegmentView.java new file mode 100644 index 000000000..befdbee42 --- /dev/null +++ b/integrations/java/pl/jakubweg/SkipSegmentView.java @@ -0,0 +1,95 @@ +package pl.jakubweg; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.lang.ref.WeakReference; + +import static pl.jakubweg.Helper.getStringByName; +import static pl.jakubweg.PlayerController.VERBOSE; + +@SuppressLint({"RtlHardcoded", "SetTextI18n", "LongLogTag"}) +public class SkipSegmentView extends TextView implements View.OnClickListener { + public static final String TAG = "jakubweg.SkipSegmentView"; + private static boolean isVisible = false; + private static WeakReference view = new WeakReference<>(null); + private static SponsorSegment lastNotifiedSegment; + + public SkipSegmentView(Context context) { + super(context); + isVisible = false; + setVisibility(GONE); + view = new WeakReference<>(this); + + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.END | Gravity.RIGHT | Gravity.CENTER_VERTICAL + ); + this.setLayoutParams(layoutParams); + this.setBackgroundColor(0x66000000); +// this.setBackgroundColor(Color.MAGENTA); + this.setTextColor(0xFFFFFFFF); + int padding = (int) convertDpToPixel(4, context); + setPadding(padding, padding, padding, padding); + + this.setText("▶ " + getStringByName(context, "tap_skip")); + + setOnClickListener(this); + } + + public static void show() { + if (isVisible) return; + SkipSegmentView view = SkipSegmentView.view.get(); + if (VERBOSE) + Log.d(TAG, "show; view=" + view); + if (view != null) { + view.setVisibility(VISIBLE); + view.bringToFront(); + view.requestLayout(); + view.invalidate(); + } + isVisible = true; + } + + public static void hide() { + if (!isVisible) return; + SkipSegmentView view = SkipSegmentView.view.get(); + if (VERBOSE) + Log.d(TAG, "hide; view=" + view); + if (view != null) + view.setVisibility(GONE); + isVisible = false; + } + + public static void notifySkipped(SponsorSegment segment) { + if (segment == lastNotifiedSegment) { + if (VERBOSE) + Log.d(TAG, "notifySkipped; segment == lastNotifiedSegment"); + return; + } + lastNotifiedSegment = segment; + String skipMessage = segment.category.skipMessage; + SkipSegmentView view = SkipSegmentView.view.get(); + if (VERBOSE) + Log.d(TAG, String.format("notifySkipped; view=%s, message=%s", view, skipMessage)); + if (view != null) + Toast.makeText(view.getContext(), skipMessage, Toast.LENGTH_SHORT).show(); + } + + public static float convertDpToPixel(float dp, Context context) { + return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + @Override + public void onClick(View v) { + PlayerController.onSkipSponsorClicked(); + } +} diff --git a/integrations/java/pl/jakubweg/SponsorBlockPreferenceFragment.java b/integrations/java/pl/jakubweg/SponsorBlockPreferenceFragment.java new file mode 100644 index 000000000..38e98af9b --- /dev/null +++ b/integrations/java/pl/jakubweg/SponsorBlockPreferenceFragment.java @@ -0,0 +1,249 @@ +package pl.jakubweg; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.text.InputType; +import android.widget.Toast; + +import java.io.File; +import java.util.ArrayList; + +import static pl.jakubweg.Helper.getStringByName; +import static pl.jakubweg.SponsorBlockSettings.DefaultBehaviour; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_CACHE_SEGMENTS; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_COUNT_SKIPS; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_NEW_SEGMENT_ENABLED; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_UUID; +import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_NAME; +import static pl.jakubweg.SponsorBlockSettings.adjustNewSegmentMillis; +import static pl.jakubweg.SponsorBlockSettings.cacheEnabled; +import static pl.jakubweg.SponsorBlockSettings.countSkips; +import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically; +import static pl.jakubweg.SponsorBlockSettings.uuid; + +public class SponsorBlockPreferenceFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + + private ArrayList preferencesToDisableWhenSBDisabled = new ArrayList<>(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getPreferenceManager().setSharedPreferencesName(PREFERENCES_NAME); + + getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + + Activity context = this.getActivity(); + + PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); + setPreferenceScreen(preferenceScreen); + + { + SwitchPreference preference = new SwitchPreference(context); + preferenceScreen.addPreference(preference); + preference.setKey(PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED); + preference.setDefaultValue(SponsorBlockSettings.isSponsorBlockEnabled); + preference.setChecked(SponsorBlockSettings.isSponsorBlockEnabled); + preference.setTitle(getStringByName(context, "enable_sb")); + preference.setSummary(getStringByName(context, "enable_sb_sum")); + preference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + enableCategoriesIfNeeded(((Boolean) newValue)); + return true; + } + }); + } + + { + SwitchPreference preference = new SwitchPreference(context); + preferenceScreen.addPreference(preference); + preference.setKey(PREFERENCES_KEY_NEW_SEGMENT_ENABLED); + preference.setDefaultValue(SponsorBlockSettings.isAddNewSegmentEnabled); + preference.setChecked(SponsorBlockSettings.isAddNewSegmentEnabled); + preference.setTitle(getStringByName(context, "enable_segmadding")); + preference.setSummary(getStringByName(context, "enable_segmadding_sum")); + preferencesToDisableWhenSBDisabled.add(preference); + } + + addGeneralCategory(context, preferenceScreen); + addSegmentsCategory(context, preferenceScreen); + addAboutCategory(context, preferenceScreen); + + enableCategoriesIfNeeded(SponsorBlockSettings.isSponsorBlockEnabled); + } + + private void enableCategoriesIfNeeded(boolean enabled) { + for (Preference preference : preferencesToDisableWhenSBDisabled) + preference.setEnabled(enabled); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + private void addSegmentsCategory(Context context, PreferenceScreen screen) { + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + preferencesToDisableWhenSBDisabled.add(category); + category.setTitle(getStringByName(context, "diff_segments")); + + String defaultValue = DefaultBehaviour.key; + 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; + entryValues[i] = behaviour.key; + } + + for (SponsorBlockSettings.SegmentInfo segmentInfo : SponsorBlockSettings.SegmentInfo.valuesWithoutPreview()) { + ListPreference preference = new ListPreference(context); + preference.setTitle(segmentInfo.getTitleWithDot()); + preference.setSummary(segmentInfo.description); + preference.setKey(segmentInfo.key); + preference.setDefaultValue(defaultValue); + preference.setEntries(entries); + preference.setEntryValues(entryValues); + category.addPreference(preference); + } + + } + + private void addAboutCategory(Context context, PreferenceScreen screen) { + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + category.setTitle("About"); + + { + Preference preference = new Preference(context); + screen.addPreference(preference); + preference.setTitle(getStringByName(context, "about_api")); + preference.setSummary(getStringByName(context, "about_api_sum")); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("http://sponsor.ajay.app")); + preference.getContext().startActivity(i); + return false; + } + }); + } + + { + Preference preference = new Preference(context); + screen.addPreference(preference); + preference.setTitle(getStringByName(context, "about_madeby")); + } + + } + + private void addGeneralCategory(final Context context, PreferenceScreen screen) { + final PreferenceCategory category = new PreferenceCategory(context); + preferencesToDisableWhenSBDisabled.add(category); + screen.addPreference(category); + category.setTitle(getStringByName(context, "general")); + + { + Preference preference = new SwitchPreference(context); + preference.setTitle(getStringByName(context, "general_skiptoast")); + preference.setSummary(getStringByName(context, "general_skiptoast_sum")); + preference.setKey(PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP); + preference.setDefaultValue(showToastWhenSkippedAutomatically); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Toast.makeText(preference.getContext(), getStringByName(context, "skipped_segment"), Toast.LENGTH_SHORT).show(); + return false; + } + }); + preferencesToDisableWhenSBDisabled.add(preference); + screen.addPreference(preference); + } + + { + Preference preference = new SwitchPreference(context); + preference.setTitle(getStringByName(context, "general_skipcount")); + preference.setSummary(getStringByName(context, "general_skipcount_sum")); + preference.setKey(PREFERENCES_KEY_COUNT_SKIPS); + preference.setDefaultValue(countSkips); + preferencesToDisableWhenSBDisabled.add(preference); + screen.addPreference(preference); + } + + { + EditTextPreference preference = new EditTextPreference(context); + preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); + preference.setTitle(getStringByName(context, "general_adjusting")); + preference.setSummary(getStringByName(context, "general_adjusting_sum")); + preference.setKey(PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP); + preference.setDefaultValue(String.valueOf(adjustNewSegmentMillis)); + screen.addPreference(preference); + preferencesToDisableWhenSBDisabled.add(preference); + } + + { + Preference preference = new EditTextPreference(context); + preference.setTitle(getStringByName(context, "general_uuid")); + preference.setSummary(getStringByName(context, "general_uuid_sum")); + preference.setKey(PREFERENCES_KEY_UUID); + preference.setDefaultValue(uuid); + screen.addPreference(preference); + preferencesToDisableWhenSBDisabled.add(preference); + } + + { + Preference preference = new SwitchPreference(context); + preference.setTitle(getStringByName(context, "general_cache")); + preference.setSummary(getStringByName(context, "general_cache_sum")); + preference.setKey(PREFERENCES_KEY_CACHE_SEGMENTS); + preference.setDefaultValue(cacheEnabled); + screen.addPreference(preference); + preferencesToDisableWhenSBDisabled.add(preference); + } + + { + Preference preference = new Preference(context); + preference.setTitle(getStringByName(context, "general_cache_clear")); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + File cacheDirectory = SponsorBlockSettings.cacheDirectory; + if (cacheDirectory != null) { + for (File file : cacheDirectory.listFiles()) { + if (!file.delete()) + return false; + } + Toast.makeText(getActivity(), getStringByName(context, "done"), Toast.LENGTH_SHORT).show(); + } + return false; + } + }); + preferencesToDisableWhenSBDisabled.add(preference); + screen.addPreference(preference); + } + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + SponsorBlockSettings.update(getActivity()); + } +} diff --git a/integrations/java/pl/jakubweg/SponsorBlockSettings.java b/integrations/java/pl/jakubweg/SponsorBlockSettings.java new file mode 100644 index 000000000..3aeb5b817 --- /dev/null +++ b/integrations/java/pl/jakubweg/SponsorBlockSettings.java @@ -0,0 +1,215 @@ +package pl.jakubweg; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.text.Html; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static pl.jakubweg.Helper.getStringByName; + +public class SponsorBlockSettings { + + public static final String CACHE_DIRECTORY_NAME = "sponsor-block-segments-1"; + public static final String PREFERENCES_NAME = "sponsor-block"; + public static final String PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP = "show-toast"; + public static final String PREFERENCES_KEY_COUNT_SKIPS = "count-skips"; + public static final String PREFERENCES_KEY_UUID = "uuid"; + public static final String PREFERENCES_KEY_CACHE_SEGMENTS = "cache-enabled"; + public static final String PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP = "new-segment-step-accuracy"; + public static final String PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED = "sb-enabled"; + public static final String PREFERENCES_KEY_NEW_SEGMENT_ENABLED = "sb-new-segment-enabled"; + public static final String sponsorBlockSkipSegmentsUrl = "https://sponsor.ajay.app/api/skipSegments"; + public static final String sponsorBlockViewedUrl = "https://sponsor.ajay.app/api/viewedVideoSponsorTime"; + public static final SegmentBehaviour DefaultBehaviour = SegmentBehaviour.SkipAutomatically; + public static boolean isSponsorBlockEnabled = false; + public static boolean isAddNewSegmentEnabled = false; + public static boolean showToastWhenSkippedAutomatically = true; + public static boolean countSkips = true; + public static boolean cacheEnabled = true; + public static int adjustNewSegmentMillis = 150; + public static String uuid = ""; + public static File cacheDirectory; + static Context context; + private static String sponsorBlockUrlCategories = "[]"; + + public SponsorBlockSettings(Context context) { + SponsorBlockSettings.context = context; + } + + public static String getSponsorBlockUrlWithCategories(String videoId) { + return sponsorBlockSkipSegmentsUrl + "?videoID=" + videoId + "&categories=" + sponsorBlockUrlCategories; + } + + public static String getSponsorBlockViewedUrl(String UUID) { + return sponsorBlockViewedUrl + "?UUID=" + UUID; + } + + public static void update(Context context) { + if (context == null) return; + File directory = cacheDirectory = new File(context.getCacheDir(), CACHE_DIRECTORY_NAME); + if (!directory.mkdirs() && !directory.exists()) { + Log.e("jakubweg.Settings", "Unable to create cache directory"); + cacheDirectory = null; + } + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); + isSponsorBlockEnabled = preferences.getBoolean(PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, isSponsorBlockEnabled); + if (!isSponsorBlockEnabled) { + SkipSegmentView.hide(); + NewSegmentHelperLayout.hide(); + SponsorBlockUtils.hideButton(); + PlayerController.sponsorSegmentsOfCurrentVideo = null; + } else if (isAddNewSegmentEnabled) { + SponsorBlockUtils.showButton(); + } + + isAddNewSegmentEnabled = preferences.getBoolean(PREFERENCES_KEY_NEW_SEGMENT_ENABLED, isAddNewSegmentEnabled); + if (!isAddNewSegmentEnabled) { + NewSegmentHelperLayout.hide(); + SponsorBlockUtils.hideButton(); + } else { + SponsorBlockUtils.showButton(); + } + + SegmentBehaviour[] possibleBehaviours = SegmentBehaviour.values(); + final ArrayList enabledCategories = new ArrayList<>(possibleBehaviours.length); + for (SegmentInfo segment : SegmentInfo.valuesWithoutPreview()) { + SegmentBehaviour behaviour = null; + String value = preferences.getString(segment.key, null); + if (value == null) + behaviour = DefaultBehaviour; + else { + for (SegmentBehaviour possibleBehaviour : possibleBehaviours) { + if (possibleBehaviour.key.equals(value)) { + behaviour = possibleBehaviour; + break; + } + } + } + if (behaviour == null) + behaviour = DefaultBehaviour; + + segment.behaviour = behaviour; + if (behaviour.showOnTimeBar) + enabledCategories.add(segment.key); + } + + //"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22]"; + if (enabledCategories.size() == 0) + sponsorBlockUrlCategories = "[]"; + else + sponsorBlockUrlCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]"; + + + showToastWhenSkippedAutomatically = preferences.getBoolean(PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP, showToastWhenSkippedAutomatically); + cacheEnabled = preferences.getBoolean(PREFERENCES_KEY_CACHE_SEGMENTS, true); + adjustNewSegmentMillis = Integer.parseInt(preferences + .getString(PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP, + String.valueOf(adjustNewSegmentMillis))); + + + uuid = preferences.getString(PREFERENCES_KEY_UUID, null); + if (uuid == null) { + uuid = (UUID.randomUUID().toString() + + UUID.randomUUID().toString() + + UUID.randomUUID().toString()) + .replace("-", ""); + preferences.edit().putString(PREFERENCES_KEY_UUID, uuid).apply(); + } + } + + public enum SegmentBehaviour { + SkipAutomatically("skip", getStringByName(context, "skip_automatically"), true, true), + ManualSkip("manual-skip", getStringByName(context, "skip_showbutton"), false, true), + Ignore("ignore", getStringByName(context, "skip_ignore"), false, false); + + public final String key; + public final String name; + public final boolean skip; + public final boolean showOnTimeBar; + + SegmentBehaviour(String key, + String name, + boolean skip, + boolean showOnTimeBar) { + this.key = key; + this.name = name; + this.skip = skip; + this.showOnTimeBar = showOnTimeBar; + } + } + + public enum SegmentInfo { + Sponsor("sponsor", getStringByName(context, "segments_sponsor"), getStringByName(context, "skipped_sponsor"), getStringByName(context, "segments_sponsor_sum"), null, 0xFF00d400), + Intro("intro", getStringByName(context, "segments_intermission"), getStringByName(context, "skipped_intermission"), getStringByName(context, "segments_intermission_sum"), null, 0xFF00ffff), + Outro("outro", getStringByName(context, "segments_endcard"), getStringByName(context, "skipped_endcard"), getStringByName(context, "segments_endcards_sum"), null, 0xFF0202ed), + Interaction("interaction", getStringByName(context, "segments_subscribe"), getStringByName(context, "skipped_subscribe"), getStringByName(context, "segments_subscribe_sum"), null, 0xFFcc00ff), + SelfPromo("selfpromo", getStringByName(context, "segments_selfpromo"), getStringByName(context, "skipped_selfpromo"), getStringByName(context, "segments_selfpromo_sum"), null, 0xFFffff00), + MusicOfftopic("music_offtopic", getStringByName(context, "segments_music"), getStringByName(context, "skipped_music"), getStringByName(context, "segments_music_sum"), null, 0xFFff9900), + Preview("preview", "", getStringByName(context, "skipped_preview"), "", SegmentBehaviour.SkipAutomatically, 0xFF000000), + ; + + private static SegmentInfo[] mValuesWithoutPreview = new SegmentInfo[]{ + Sponsor, + Intro, + Outro, + Interaction, + SelfPromo, + MusicOfftopic + }; + private static Map mValuesMap = new HashMap<>(7); + + static { + for (SegmentInfo value : valuesWithoutPreview()) + mValuesMap.put(value.key, value); + } + + public final String key; + public final String title; + public final String skipMessage; + public final String description; + public final int color; + public final Paint paint; + public SegmentBehaviour behaviour; + private CharSequence lazyTitleWithDot; + + SegmentInfo(String key, + String title, + String skipMessage, + String description, + SegmentBehaviour behaviour, + int color) { + + this.key = key; + this.title = title; + this.skipMessage = skipMessage; + this.description = description; + this.behaviour = behaviour; + this.color = color & 0xFFFFFF; + paint = new Paint(); + paint.setColor(color); + } + + public static SegmentInfo[] valuesWithoutPreview() { + return mValuesWithoutPreview; + } + + public static SegmentInfo byCategoryKey(String key) { + return mValuesMap.get(key); + } + + public CharSequence getTitleWithDot() { + return (lazyTitleWithDot == null) ? + lazyTitleWithDot = Html.fromHtml(String.format(" %s", color, title)) + : lazyTitleWithDot; + } + } +} diff --git a/integrations/java/pl/jakubweg/SponsorBlockUtils.java b/integrations/java/pl/jakubweg/SponsorBlockUtils.java new file mode 100644 index 000000000..d145d1205 --- /dev/null +++ b/integrations/java/pl/jakubweg/SponsorBlockUtils.java @@ -0,0 +1,648 @@ +package pl.jakubweg; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.RandomAccessFile; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; +import java.util.TimeZone; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static pl.jakubweg.PlayerController.VERBOSE; +import static pl.jakubweg.PlayerController.getCurrentVideoId; +import static pl.jakubweg.PlayerController.getLastKnownVideoTime; +import static pl.jakubweg.PlayerController.sponsorSegmentsOfCurrentVideo; +import static pl.jakubweg.SponsorBlockSettings.sponsorBlockSkipSegmentsUrl; + +@SuppressWarnings({"LongLogTag"}) +public abstract class SponsorBlockUtils { + public static final String TAG = "jakubweg.SponsorBlockUtils"; + public static final String DATE_FORMAT = "HH:mm:ss.SSS"; + @SuppressLint("SimpleDateFormat") + public static final SimpleDateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT); + private static final int sponsorBtnId = 1234; + private static final View.OnClickListener sponsorBlockBtnListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + NewSegmentHelperLayout.toggle(); + } + }; + private static int shareBtnId = -1; + private static long newSponsorSegmentDialogShownMillis; + private static long newSponsorSegmentStartMillis = -1; + private static long newSponsorSegmentEndMillis = -1; + 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(), "Start of the segment set", Toast.LENGTH_LONG).show(); + break; + case DialogInterface.BUTTON_POSITIVE: + // end + newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis; + Toast.makeText(context.getApplicationContext(), "End of the segment set", Toast.LENGTH_SHORT).show(); + break; + } + dialog.dismiss(); + } + }; + private static SponsorBlockSettings.SegmentInfo newSponsorBlockSegmentType; + private static final DialogInterface.OnClickListener segmentTypeListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockSettings.SegmentInfo.valuesWithoutPreview()[which]; + boolean enableButton; + if (!segmentType.behaviour.showOnTimeBar) { + Toast.makeText( + ((AlertDialog) dialog).getContext().getApplicationContext(), + "You've disabled this category in the settings, so can't submit it", + Toast.LENGTH_SHORT).show(); + enableButton = false; + } else { + Toast.makeText( + ((AlertDialog) dialog).getContext().getApplicationContext(), + segmentType.description, + Toast.LENGTH_SHORT).show(); + newSponsorBlockSegmentType = segmentType; + enableButton = true; + } + + ((AlertDialog) dialog) + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(enableButton); + } + }; + 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(); + + SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutPreview(); + CharSequence[] titles = new CharSequence[values.length]; + for (int i = 0; i < values.length; i++) { +// titles[i] = values[i].title; + titles[i] = values[i].getTitleWithDot(); + } + + newSponsorBlockSegmentType = null; + new AlertDialog.Builder(context) + .setTitle("Choose the segment 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, "Submitting segment...", Toast.LENGTH_SHORT).show(); + + appContext = new WeakReference<>(context); + new Thread(submitRunnable).start(); + } + }; + private static boolean isShown = false; + private static WeakReference sponsorBlockBtn = new WeakReference<>(null); + private static String messageToToast = ""; + private static EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener(); + private static final DialogInterface.OnClickListener editByHandDialogListener = new DialogInterface.OnClickListener() { + @SuppressLint("DefaultLocale") + @Override + public void onClick(DialogInterface dialog, int which) { + Context context = ((AlertDialog) dialog).getContext(); + + 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))); + } + + editByHandSaveDialogListener.settingStart = isStart; + editByHandSaveDialogListener.editText = new WeakReference<>(textView); + new AlertDialog.Builder(context) + .setTitle("Time of the " + (isStart ? "start" : "end") + " of the segment") + .setView(textView) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton("now", editByHandSaveDialogListener) + .setPositiveButton(android.R.string.ok, editByHandSaveDialogListener) + .show(); + + dialog.dismiss(); + } + }; + private static Runnable toastRunnable = new Runnable() { + @Override + public void run() { + Context context = appContext.get(); + if (context != null && messageToToast != null) + Toast.makeText(context, messageToToast, Toast.LENGTH_LONG).show(); + } + }; + private static final Runnable submitRunnable = new Runnable() { + @Override + public void run() { + messageToToast = null; + final String uuid = SponsorBlockSettings.uuid; + 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) { + Log.e(TAG, "Unable to submit times, invalid parameters"); + return; + } + + URL url = new URL(String.format(Locale.US, + sponsorBlockSkipSegmentsUrl + "?videoID=%s&userID=%s&startTime=%.3f&endTime=%.3f&category=%s", + videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key)); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + switch (connection.getResponseCode()) { + default: + messageToToast = "Unable to submit segments: Status: " + connection.getResponseCode() + " " + connection.getResponseMessage(); + break; + case 429: + messageToToast = "Can't submit the segment.\nRate Limit (Too many for the same user or IP)"; + break; + case 403: + messageToToast = "Can't submit the segment.\nRejected by auto moderator"; + break; + case 409: + messageToToast = "Duplicate"; + break; + case 200: + messageToToast = "Segment submitted successfully"; + break; + } + + Log.i(TAG, "Segment submitted with status: " + connection.getResponseCode() + ", " + messageToToast); + new Handler(Looper.getMainLooper()).post(toastRunnable); + + connection.disconnect(); + + newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1; + } catch (Exception e) { + Log.e(TAG, "Unable to submit segment", e); + } + + if (videoId != null) + PlayerController.executeDownloadSegments(videoId, true); + } + }; + + static { + dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + private SponsorBlockUtils() { + } + + public static void showButton() { + if (isShown) return; + isShown = true; + View i = sponsorBlockBtn.get(); + if (i == null) return; + i.setVisibility(VISIBLE); + i.bringToFront(); + i.requestLayout(); + i.invalidate(); + } + + public static void hideButton() { + if (!isShown) return; + isShown = false; + View i = sponsorBlockBtn.get(); + if (i != null) + i.setVisibility(GONE); + } + + @SuppressLint("LongLogTag") + public static void addImageButton(final Activity activity, final int attemptsWhenFail) { + if (VERBOSE) + Log.d(TAG, "addImageButton activity=" + activity + ",attemptsWhenFail=" + attemptsWhenFail); + + if (activity == null) + return; + + final View existingSponsorBtn = activity.findViewById(sponsorBtnId); + if (existingSponsorBtn != null) { + if (VERBOSE) + Log.d(TAG, "addImageButton: sponsorBtn exists"); + if (SponsorBlockSettings.isAddNewSegmentEnabled) + showButton(); + return; + } + + String packageName = activity.getPackageName(); + Resources R = activity.getResources(); + shareBtnId = R.getIdentifier("player_share_button", "id", packageName); +// final int addToBtnId = R.getIdentifier("player_addto_button", "id", packageName); + final int addToBtnId = R.getIdentifier("live_chat_overlay_button", "id", packageName); + int titleViewId = R.getIdentifier("player_video_title_view", "id", packageName); +// final int iconId = R.getIdentifier("player_fast_forward", "drawable", packageName); + final int iconId = R.getIdentifier("ic_sb_logo", "drawable", packageName); + + + final View addToBtn = activity.findViewById(addToBtnId); + final ImageView shareBtn = activity.findViewById(shareBtnId); + final TextView titleView = activity.findViewById(titleViewId); + + if (addToBtn == null || shareBtn == null || titleView == null) { + if (VERBOSE) + Log.e(TAG, String.format("one of following is null: addToBtn=%s shareBtn=%s titleView=%s", + addToBtn, shareBtn, titleView)); + + if (attemptsWhenFail > 0) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + if (VERBOSE) + Log.i(TAG, "Retrying addImageButton"); + addImageButton(PlayerController.playerActivity.get(), attemptsWhenFail - 1); + } + }, 5000); + return; + } + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + try { + + Class touchImageViewClass = Class.forName("com.google.android.libraries.youtube.common.ui.TouchImageView"); + Constructor constructor = touchImageViewClass.getConstructor(Context.class); + final ImageView instance = ((ImageView) constructor.newInstance(activity)); + instance.setImageResource(iconId); + instance.setId(sponsorBtnId); + + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(shareBtn.getLayoutParams()); + layoutParams.addRule(RelativeLayout.LEFT_OF, addToBtnId); + + instance.setLayoutParams(layoutParams); + ((ViewGroup) shareBtn.getParent()).addView(instance, 0); + + + instance.setPadding(shareBtn.getPaddingLeft(), + shareBtn.getPaddingTop(), + shareBtn.getPaddingRight(), + shareBtn.getPaddingBottom()); + + + RelativeLayout.LayoutParams titleViewLayoutParams = (RelativeLayout.LayoutParams) titleView.getLayoutParams(); + titleViewLayoutParams.addRule(RelativeLayout.START_OF, sponsorBtnId); + titleView.requestLayout(); + + instance.setClickable(true); + instance.setFocusable(true); + Drawable.ConstantState constantState = shareBtn.getBackground().mutate().getConstantState(); + if (constantState != null) + instance.setBackground(constantState.newDrawable()); + + instance.setOnClickListener(sponsorBlockBtnListener); + sponsorBlockBtn = new WeakReference<>(instance); + isShown = true; + if (!SponsorBlockSettings.isAddNewSegmentEnabled) + hideButton(); + if (VERBOSE) + Log.i(TAG, "Image Button added"); + } catch (Exception e) { + Log.e(TAG, "Error while adding button", e); + } + } + }); + } + + @SuppressLint("DefaultLocale") + public static void onMarkLocationClicked(Context context) { + newSponsorSegmentDialogShownMillis = PlayerController.getLastKnownVideoTime(); + + new AlertDialog.Builder(context) + .setTitle("New Sponsor Block segment") + .setMessage(String.format("Set %02d:%02d:%04d as a start or end of new segment?", + newSponsorSegmentDialogShownMillis / 60000, + newSponsorSegmentDialogShownMillis / 1000 % 60, + newSponsorSegmentDialogShownMillis % 1000)) + .setNeutralButton("Cancel", null) + .setNegativeButton("Start", newSponsorSegmentDialogListener) + .setPositiveButton("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("Is it right?") + .setMessage(String.format("The segment lasts from %02d:%02d to %02d:%02d (%d minutes %02d seconds)\nIs it ready to submit?", + 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 { + Toast.makeText(context, "Mark two locations on the time bar first", Toast.LENGTH_SHORT).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.Preview, null); + + Arrays.sort(segments); + sponsorSegmentsOfCurrentVideo = segments; + } else { + Toast.makeText(context, "Mark two locations on the time bar first", Toast.LENGTH_SHORT).show(); + } + } + + @SuppressLint("DefaultLocale") + public static void onEditByHandClicked(Context context) { + new AlertDialog.Builder(context) + .setTitle("Edit time of new segment by hand") + .setMessage("Do you want to edit time of the start or the end of the segment?") + .setNeutralButton(android.R.string.cancel, null) + .setNegativeButton("start", editByHandDialogListener) + .setPositiveButton("end", editByHandDialogListener) + .show(); + } + + public static void notifyShareBtnVisibilityChanged(View v) { + if (v.getId() != shareBtnId || !SponsorBlockSettings.isAddNewSegmentEnabled) return; +// if (VERBOSE) +// Log.d(TAG, "VISIBILITY CHANGED of view " + v); + ImageView sponsorBtn = sponsorBlockBtn.get(); + if (sponsorBtn != null) { + sponsorBtn.setVisibility(v.getVisibility()); + } + } + + public synchronized static SponsorSegment[] getSegmentsForVideo(String videoId, boolean ignoreCache) { + newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1; + + int usageCounter = 0; + if (!ignoreCache && SponsorBlockSettings.cacheEnabled) { + + File cacheDirectory = SponsorBlockSettings.cacheDirectory; + if (cacheDirectory == null) { + Log.w(TAG, "Cache directory is null, cannot read"); + } else { + File file = new File(cacheDirectory, videoId); + try { + RandomAccessFile rwd = new RandomAccessFile(file, "rw"); + rwd.seek(0); + usageCounter = rwd.readInt(); + long now = System.currentTimeMillis(); + long savedTimestamp = rwd.readLong(); + int segmentsSize = rwd.readInt(); + byte maxDaysCache; + if (usageCounter < 2) + maxDaysCache = 0; + else if (usageCounter < 5 || segmentsSize == 0) + maxDaysCache = 2; + else if (usageCounter < 10) + maxDaysCache = 5; + else + maxDaysCache = 10; + + + if (VERBOSE) + Log.d(TAG, String.format("Read cache data about segments, counter=%d, timestamp=%d, now=%d, maxCacheDays=%s, segmentsSize=%d", + usageCounter, savedTimestamp, now, maxDaysCache, segmentsSize)); + + if (savedTimestamp + (((long) maxDaysCache) * 24 * 60 * 60 * 1000) > now) { + if (VERBOSE) + Log.d(TAG, "getSegmentsForVideo: cacheHonored videoId=" + videoId); + + SponsorSegment[] segments = new SponsorSegment[segmentsSize]; + for (int i = 0; i < segmentsSize; i++) { + segments[i] = SponsorSegment.readFrom(rwd); + } + + rwd.seek(0); + rwd.writeInt(usageCounter + 1); + rwd.close(); + if (VERBOSE) + Log.d(TAG, "getSegmentsForVideo: reading from cache and updating usageCounter finished"); + + return segments; + } else { + if (VERBOSE) + Log.d(TAG, "getSegmentsForVideo: cache of video " + videoId + " was not honored, fallback to downloading..."); + } + } catch (FileNotFoundException | EOFException ignored) { + if (VERBOSE) + Log.e(TAG, "FileNotFoundException | EOFException ignored"); + } catch (Exception e) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + Log.e(TAG, "Error while reading cached segments", e); + } + } + } + + ArrayList sponsorSegments = new ArrayList<>(); + try { + if (VERBOSE) + Log.i(TAG, "Trying to download segments for videoId=" + videoId); + + URL url = new URL(SponsorBlockSettings.getSponsorBlockUrlWithCategories(videoId)); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + switch (connection.getResponseCode()) { + default: + Log.e(TAG, "Unable to download segments: Status: " + connection.getResponseCode() + " " + connection.getResponseMessage()); + break; + case 404: + Log.w(TAG, "No segments for this video (ERR404)"); + break; + case 200: + if (VERBOSE) + Log.i(TAG, "Received status 200 OK, parsing response..."); + + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + connection.getInputStream().close(); + + + JSONArray responseArray = new JSONArray(stringBuilder.toString()); + int length = responseArray.length(); + for (int i = 0; i < length; i++) { + JSONObject obj = ((JSONObject) responseArray.get(i)); + JSONArray segments = obj.getJSONArray("segment"); + long start = (long) (segments.getDouble(0) * 1000); + long end = (long) (segments.getDouble(1) * 1000); + String category = obj.getString("category"); + String UUID = obj.getString("UUID"); + + SponsorBlockSettings.SegmentInfo segmentCategory = SponsorBlockSettings.SegmentInfo.byCategoryKey(category); + if (segmentCategory != null && segmentCategory.behaviour.showOnTimeBar) { + SponsorSegment segment = new SponsorSegment(start, end, segmentCategory, UUID); + sponsorSegments.add(segment); + } + } + + if (VERBOSE) + Log.v(TAG, "Parsing done"); + break; + } + + connection.disconnect(); + + if (SponsorBlockSettings.cacheEnabled) { + File cacheDirectory = SponsorBlockSettings.cacheDirectory; + if (cacheDirectory == null) { + Log.w(TAG, "Cache directory is null"); + } else { + File file = new File(cacheDirectory, videoId); + try { + DataOutputStream stream = new DataOutputStream(new FileOutputStream(file)); + stream.writeInt(usageCounter + 1); + stream.writeLong(System.currentTimeMillis()); + stream.writeInt(sponsorSegments.size()); + for (SponsorSegment segment : sponsorSegments) { + segment.writeTo(stream); + } + stream.close(); + } catch (Exception e) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + Log.e(TAG, "Unable to write segments to file", e); + } + } + } + + } catch (Exception e) { + Log.e(TAG, "download segments failed", e); + } + + return sponsorSegments.toArray(new SponsorSegment[0]); + } + + public static void sendViewCountRequest(SponsorSegment segment) { + try { + URL url = new URL(SponsorBlockSettings.getSponsorBlockViewedUrl(segment.UUID)); + + Log.d("sponsorblock", "requesting: " + url.getPath()); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.getInputStream().close(); + connection.disconnect(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener { + public boolean settingStart; + public 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 { + long time = (which == DialogInterface.BUTTON_NEUTRAL) ? + getLastKnownVideoTime() : + (Objects.requireNonNull(dateFormatter.parse(editText.getText().toString())).getTime()); + + if (settingStart) + newSponsorSegmentStartMillis = Math.max(time, 0); + else + newSponsorSegmentEndMillis = time; + + if (which == DialogInterface.BUTTON_NEUTRAL) + editByHandDialogListener.onClick(dialog, settingStart ? + DialogInterface.BUTTON_NEGATIVE : + DialogInterface.BUTTON_POSITIVE); + else + Toast.makeText(context.getApplicationContext(), "Done", Toast.LENGTH_SHORT).show(); + } catch (ParseException e) { + Toast.makeText(context.getApplicationContext(), "Cannot parse this time 😔", Toast.LENGTH_LONG).show(); + } + } + } + +} diff --git a/integrations/java/pl/jakubweg/SponsorSegment.java b/integrations/java/pl/jakubweg/SponsorSegment.java new file mode 100644 index 000000000..7f25c0b87 --- /dev/null +++ b/integrations/java/pl/jakubweg/SponsorSegment.java @@ -0,0 +1,49 @@ +package pl.jakubweg; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class SponsorSegment implements Comparable { + public final long start; + public final long end; + public final SponsorBlockSettings.SegmentInfo category; + public final String UUID; + + public SponsorSegment(long start, long end, SponsorBlockSettings.SegmentInfo category, String UUID) { + this.start = start; + this.end = end; + this.category = category; + this.UUID = UUID; + } + + public static SponsorSegment readFrom(RandomAccessFile stream) throws IOException { + long start = stream.readLong(); + long end = stream.readLong(); + String categoryName = stream.readUTF(); + String UUID = stream.readUTF(); + SponsorBlockSettings.SegmentInfo category = SponsorBlockSettings.SegmentInfo.valueOf(categoryName); + return new SponsorSegment(start, end, category, UUID); + } + + @Override + public String toString() { + return "SegmentInfo{" + + "start=" + start + + ", end=" + end + + ", category='" + category + '\'' + + '}'; + } + + @Override + public int compareTo(SponsorSegment o) { + return (int) (this.start - o.start); + } + + public void writeTo(DataOutputStream stream) throws IOException { + stream.writeLong(start); + stream.writeLong(end); + stream.writeUTF(category.name()); + stream.writeUTF(UUID); + } +} diff --git a/integrations/res/drawable/ic_sb_adjust.xml b/integrations/res/drawable/ic_sb_adjust.xml new file mode 100644 index 000000000..76a4b8bc9 --- /dev/null +++ b/integrations/res/drawable/ic_sb_adjust.xml @@ -0,0 +1,10 @@ + + + diff --git a/integrations/res/drawable/ic_sb_compare.xml b/integrations/res/drawable/ic_sb_compare.xml new file mode 100644 index 000000000..04cc65e40 --- /dev/null +++ b/integrations/res/drawable/ic_sb_compare.xml @@ -0,0 +1,10 @@ + + + diff --git a/integrations/res/drawable/ic_sb_edit.xml b/integrations/res/drawable/ic_sb_edit.xml new file mode 100644 index 000000000..e93574bd9 --- /dev/null +++ b/integrations/res/drawable/ic_sb_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/integrations/res/drawable/ic_sb_logo.xml b/integrations/res/drawable/ic_sb_logo.xml new file mode 100644 index 000000000..a484598e3 --- /dev/null +++ b/integrations/res/drawable/ic_sb_logo.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/integrations/res/drawable/ic_sb_publish.xml b/integrations/res/drawable/ic_sb_publish.xml new file mode 100644 index 000000000..de4e58d3a --- /dev/null +++ b/integrations/res/drawable/ic_sb_publish.xml @@ -0,0 +1,10 @@ + + + diff --git a/integrations/res/mipmap-hdpi/ic_launcher.png b/integrations/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a571e60098c92c2baca8a5df62f2929cbff01b52 GIT binary patch literal 3593 zcmV+k4)*bhP){4Q1@|o^l5vR(0JRNCL<7M6}UD`@%^5zYjRJ-VNC3qn#9n=m>>ACRx!M zlW3!lO>#0MCAqh6PU7cMP#aQ`+zp##c~|0RJc4JAuaV=qZS|vg8XJ$1pYxc-u~Q5j z%Ya4ddEvZow!floOU_jrlE84*Kfv6!kMK^%#}A$Bjrna`@pk(TS$jA@P;|iPUR-x)_r4ELtL9aUonVhI31zFsJ96 z|5S{%9|FB-SsuD=#0u1WU!W6fcXF)#63D7tvwg%1l(}|SzXh_Z(5234`w*&@ctO>g z0Aug~xs*zAjCpNau(Ul@mR~?6dNGx9Ii5MbMvmvUxeqy>$Hrrn;v8G!g*o~UV4mr_ zyWaviS4O6Kb?ksg`)0wj?E@IYiw3az(r1w37|S|7!ODxfW%>6m?!@woyJUIh_!>E$ z+vYyxcpe*%QHt~E*etx=mI~XG8~QJhRar>tNMB;pPOKRfXjGt4fkp)y6=*~XIJC&C!aaha9k7~UP9;`q;1n9prU@a%Kg%gDW+xy9n`kiOj8WIs;+T>HrW znVTomw_2Yd%+r4at4zQC3*=Z4naYE7H*Dlv4=@IEtH_H;af}t@W7@mE$1xI#XM-`% z0le3-Q}*@D@ioThJ*cgm>kVSt+=txjd2BpJDbBrpqp-xV9X6Rm?1Mh~?li96xq(IP z+n(4GTXktSt_z*meC5=$pMzMKGuIn&_IeX6Wd!2$md%l{x(|LXClGVhzqE^Oa@!*! zN%O7K8^SHD|9aoAoT4QLzF+Uh_V03V;KyQ|__-RTH(F72qnVypVei#KZ2K-7YiPS* z-4gZd>%uRm<0iGmZH|~KW<>#hP9o@UT@gje_^AR{?p(v|y8`asyNi4G?n#2V+jsBa z+uJ|m;EyHnA%QR7{z(*%+Z;Ip(Xt5n<`4yZ51n^!%L?*a=)Bt{J_b`;+~$Z7h^x@& zSBr2>_@&>%7=zp5Ho5H~6-Y@wXkpt{s9Tc+7RnfWuZC|&NO6p{m-gU%=cPw3qyB>1 zto@}!>_e`99vhEQic{;8goXMo1NA`>sch8T3@O44!$uf`IlgBj#c@Ku*!9B`7seRe z2j?cKG4R-Uj8dFidy25wu#J3>-_u`WT%NfU54JcxsJv;A^i#t!2XXn%zE=O##OXoy zwR2+M!(O12D_LUsHV)v2&TBZ*di1$c8 z+_~Oo@HcOFV&TasjNRjf*;zVV?|S@-_EXmlIG@&F!WS#yU9<_Ece?sq^L^Jf%(##= zdTOpA6uXwXx3O|`C-Dbl~`~#9yjlFN>;Yr?Kv68=F`fQLW z(x40UIAuQRN~Y|fpCi2++qHWrXd&S*NS$z8V+YP zSX7#fxfebdJfrw~mzZr!thk9BE&_eic@-9C0^nK@0o$T5nAK~CHV4fzY#KJ=^uV!D z3)jL(DDpL!TDSq`=e0v8(8`Wo_~p*6KHyT!kmCCCU48I?mw-UrBj8=Vg#?O%Z2<|C z?+4Q&W09VsK<14)vHY^n;Zi3%4Q?s4x^$3;acx76-t*K|3^MUKELf>Jew${&!(xTD_PD>KINXl?sUX;X6(}jr zKrxdFCW8)!)dz>b!b9nBj1uYxc; zCkmbfhwNZDp* zIG07ixjYK$3PNQx)KxK1*Te{mTeb}BZJ++Waj0sFgVkw&DAWDnl0pBiBWqxObPX)h z*TN!$aBLmH2kNX4xMpc!d15^*Gksy1l@P~U&INWk{u*%*5>+Aqn=LEne zClEHdguEb8oEZgNsY0NjWUMIEh&hLsm2Ght7L+H$y*w6nWjffE>tJ6IF2bRboPSlg z;8~Xh^J6|kbIX-0hD~-L?Y;aST2{Rivf_k4>}dA%URJ#mvcu^R*wO6iy{vjCWaoSe zIzRNGW!00Ad0EXUi-mouPFz-|lzU9e0x_*DNL*smDnbNRbrdEYSuu3?q}5FcaLx&n z6o+$;B9jEl3Xl|sbB;2b1fnV>B@X8tbpg!?+EPe~!#T&jf&`-3(^s5eOsfnL9BZO5 z<?!X^iNgt5T^IrT!Z1m3I3c@N#=*Wk zTtb{+Os~=ijjE^lB2QE@pTLB>vqLE(X}Ul(PxsQZDCnRJoyWpo%5ub6koe;ZUTN6o;49 z%&K@2C_+LULQSaPbZ$5a#EF|k;vjo+j;&bEgJpe=Dlb&rmCN}Yml6`FSSKkCFRPi= z31Y?SD~<-!YoCBXgYhw7kJe3M?qILPK4)%D3{=?~aXC5Wgu;<#4Lf9~Ghw37nNM&o z(80MdTm&yGb#a6!4*MJ~aIJ`eYb7HVu2r#ctB!;Bxoucjw;3~P<1wQy0q*sQ z-8i2F_l87aanncS%?9u}>B0ISxxWC)h0qo zrToFN(!i`X6lQgyd`nhvZivH_^!NKOkY(B6epkb-IT>nNDsn!@k(QQ{wh(eY$F)2L z%JK*qpF;wXQ&v$amkWn9MR zaNbc-m6G;3A@HbAhN>=FN*tK8Kuz(Oa%{~&W>Cn+r}2e4u5KK(akX-yq^zQ4DCcwB zC?TsVB4vEeeSxS_^$~}*LFNtJ0!>a^k=k#8$c8T#XHavvV16Nda6bl2B5~loOSuzO zELE{i*5|lY#X(gWDdTfA@Hn5+Es&8oX6Na#Nhdn#w^HUT=U69h_kQVdztsB&!awcK zhE$2-v_uFjRBxzT6NNb)AND!l0}@y8&8iWGR`$$Kl_KCnY(6UaWtqaj6b zs*e#kA#=_#KTn{U!{V4VXkq!qx>|~Hj2P?V{?LHuK~EOwt8K?a=Xztlp31x-RhD0*-wJ+j>Y?-0hXd`O?21C+SsD+I(m2?agwd{C zOB+u@xsG_9xP@3yLwmg%s#MkFt7;-CAxBZpA)JebBVkF?7I-#pgkwW2oEiyDaUzt} zk+4W#SNAW)n+lH6T5J8{bNxA9w|@PP^za&C{2LmVpz%AG?wzpT`>@HLcMqBD^G-9} zw>-__!0I%9ZnAe-_hZjZP4nNGYJ^AgtAO?>Uo^!N|Le+X|9-g?II=KWY+eRb@sf8iJh{v#I? zC%*LZ_}5?l+Z(UF^4EXA`uArU90SL~F%8D=fjmD#FnWw0qsQp+OdS6QzyUa+`7Q|u P00000NkvXXu0mjfP=x?Y literal 0 HcmV?d00001 diff --git a/integrations/res/values/strings.xml b/integrations/res/values/strings.xml new file mode 100644 index 000000000..5668bbe2f --- /dev/null +++ b/integrations/res/values/strings.xml @@ -0,0 +1,180 @@ + + + " + - xfileFIN (Mods, Theming, Support) + - Laura (Theming, Support) + - ZaneZam (Publishing, Support) + - KevinX8 (Neko, Support)" + About + Auto repeat in background is off + Auto repeat in background is on + Auto repeat in background + Auto repeat is not linked to Autoplay toggle + Auto repeat is linked to Autoplay toggle (Autoplay off = Auto repeat on) + Auto repeat linked to Autoplay + Auto repeat is off + Auto repeat is on + Auto repeat + Video watermark is hidden + Video watermark is shown + Video watermark + ExoPlayer v2 has to be enabled for buffer settings + Buffer settings + Cast button is hidden + Cast button is shown + Cast button + Codec override + Overrided manufacturer + Overrided model + Extra debug logging is disabled + Extra debug logging is enabled + Debug mode + Tap to set your device\'s default codec + Default codec + Tap to join Vanced on Discord + Discord Server + ExoPlayer v2 is experimental. DO NOT report errors happened when ExoPlayer v2 is enabled + Warning + Tap to enable hardware HDR + Samsung Galaxy S8+ + Video brightness will follow your device\'s brightness on HDR landscape videos + Video brightness is set to max on HDR landscape videos + HDR Max brightness + taps needed to enable hidden setting + No need, hidden setting has already been enabled + Hidden setting has been enabled + Info cards are hidden + Info cards are shown + Info cards + Layout settings + "The maximum duration of media that the player will attempt to buffer (in milliseconds) + + Default: 120000" + Maximum buffer + Select the preferred minimized video type + Minimized video type + Video only + Video with media controls + Misc + Video resolution is being overridden to max + Video resolution is following your device screen resolution + Max resolution + "The duration of media that must be buffered for playback to start or resume following a user action such as seeking (in milliseconds) + + Default: 2500" + Playback start + Select preferred video resolution on Cellular Network + Preferred video quality Cellular + Select preferred video resolution on Wi-Fi Network + Preferred video quality Wi-Fi + Select preferred video speed + Preferred video speed + "The duration of media that must be buffered for playback to resume after a rebuffer (in milliseconds). A rebuffer is defined to be caused by buffer depletion rather than a user action + + Default: 5000" + Rebuffer + Vanced settings + Tap to enable software HDR + Google Pixel XL + End screens are hidden + End screens are shown + End screens + Support links + Support + Video settings + Tap to start forcing the VP9 codec + VP9 codec not enabled + VP9 codec enabled for supported devices, disable if you encounter stuttering/slowness in videos + VP9 codec + Tap to open the XDA post + XDA thread + Wide search bar + Search bar style is defined by the app + Forcing wide search bar + Dynamic player + Dynamic player is defined automatically + Dynamic player is forced on square and vertical videos + New official theme toggle is in the General settings. This theme toggle is \"Developer\" toggle. + Theme info + Accessibility controls aren\'t displayed in the player + Accessibility controls are displayed in the player + Accessibility player + Captions aren\'t enabled automatically at 0% volume + Captions are enabled automatically at 0% volume + Auto captions + Amount of pixels excluded from swiping at the top of the display to prevent swipe controls when dragging down notifications + Swipe padding + Amount of pixels you have to swipe until detecting starts to prevent unintended swiping + Swipe threshold + Swipe controls for brightness are disabled + Swipe controls for brightness are enabled + Swipe controls for Brightness + Swipe controls for Brightness and Volume + Swipe controls + Swipe controls for volume are disabled + Swipe controls for volume are enabled + Swipe controls for Volume + Tap to open our website + Vanced website + Home ads are hidden + Home ads are shown + Home ads (Experimental) + Stories are hidden + Stories are shown + YouTube stories (Experimental) + Ad settings + Credits for people who have contributed + Credits + Home ads removing enhancement and showed other kinds of debugging methods + souramoo + SponsorBlock implementation + JakubWeg + + Enable Sponsor Block (Beta) + Switch this on for very cool sponsor segments skipping + Enable new segment adding + Switch this on to enable experimental segment adding (has button visibility issues). + What to do with different segments + General + Show a toast when skipping segment automatically + Click to see an example toast + Skip count tracking + This lets SponsorBlock leaderboard system know how much time people have saved. The extension sends a message to the server each time you skip a segment. + Adjusting new segment step + This is a number of milliseconds you can move when clicking buttons when adding new segment + Your unique user id + This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you + Cache segments locally + Frequently watched videos (eg. music videos) may store segments on this device to make skipping segments faster + Clear sponsor block segments cache + Sponsor + Paid promotion, paid referrals and direct advertisements + Intermission/Intro Animation + An interval without actual content. Could be a pause, static frame, repeating animation + Endcards/Credits + Credits or when the YouTube endcards appear. Not spoken conclusions + Interaction Reminder (Subscribe) + When there is a short reminder to like, subscribe or follow them in the middle of content + Unpaid/Self Promotion + Similar to "sponsor" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated with + Music: Non-Music Section + Only for use in music videos. This includes introductions or outros in music videos + Skipped a sponsor segment + Skipped sponsor + Skipped intro + Skipped outro + Skipped annoying reminder + Skipped self promotion + Skipped silence + Skipped preview + Just skip, automatically + Show skip button + Don\'t do anything + About + This app uses API from Sponsor Block + Click to learn more at: sponsor.ajay.app + Integration made by JakubWeg + Tap to skip + + + \ No newline at end of file