Initial commit

This commit is contained in:
jakweg 2020-08-24 17:47:57 +02:00
commit ac2e3937ce
17 changed files with 2252 additions and 0 deletions

View file

@ -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 "";
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<NewSegmentHelperLayout> 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;
}
}
}

View file

@ -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<Activity> playerActivity = new WeakReference<>(null);
public static SponsorSegment[] sponsorSegmentsOfCurrentVideo;
private static WeakReference<Object> 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;
}
}
}

View file

@ -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<SkipSegmentView> 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();
}
}

View file

@ -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<Preference> 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());
}
}

View file

@ -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 = "<invalid>";
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<String> 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<String, SegmentInfo> 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("<font color=\"#%06X\">⬤</font> %s", color, title))
: lazyTitleWithDot;
}
}
}

View file

@ -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<Context> 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<ImageView> 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<SponsorSegment> 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> 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();
}
}
}
}

View file

@ -0,0 +1,49 @@
package pl.jakubweg;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
public class SponsorSegment implements Comparable<SponsorSegment> {
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);
}
}