commit 4a411e1c985427e953e85474394b76114149f340 Author: jakweg Date: Mon Aug 24 17:47:57 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4c52d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/build +/captures +.externalNativeBuild +.cxx +/.idea diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f6eb4ab --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.1" + + defaultConfig { + applicationId "pl.jakubweg" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation 'androidx.appcompat:appcompat:1.2.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..80c110c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/pl/jakubweg/Helper.java b/app/src/main/java/pl/jakubweg/Helper.java new file mode 100644 index 0000000..6ded2a5 --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/InjectedPlugin.java b/app/src/main/java/pl/jakubweg/InjectedPlugin.java new file mode 100644 index 0000000..021038f --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/NewSegmentHelperLayout.java b/app/src/main/java/pl/jakubweg/NewSegmentHelperLayout.java new file mode 100644 index 0000000..5c0ac4c --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/PlayerController.java b/app/src/main/java/pl/jakubweg/PlayerController.java new file mode 100644 index 0000000..db7bc43 --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/SkipSegmentView.java b/app/src/main/java/pl/jakubweg/SkipSegmentView.java new file mode 100644 index 0000000..befdbee --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java b/app/src/main/java/pl/jakubweg/SponsorBlockPreferenceFragment.java new file mode 100644 index 0000000..38e98af --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java b/app/src/main/java/pl/jakubweg/SponsorBlockSettings.java new file mode 100644 index 0000000..3aeb5b8 --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java b/app/src/main/java/pl/jakubweg/SponsorBlockUtils.java new file mode 100644 index 0000000..d145d12 --- /dev/null +++ b/app/src/main/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/app/src/main/java/pl/jakubweg/SponsorSegment.java b/app/src/main/java/pl/jakubweg/SponsorSegment.java new file mode 100644 index 0000000..7f25c0b --- /dev/null +++ b/app/src/main/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/app/src/main/res/drawable/ic_sb_adjust.xml b/app/src/main/res/drawable/ic_sb_adjust.xml new file mode 100644 index 0000000..76a4b8b --- /dev/null +++ b/app/src/main/res/drawable/ic_sb_adjust.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sb_compare.xml b/app/src/main/res/drawable/ic_sb_compare.xml new file mode 100644 index 0000000..04cc65e --- /dev/null +++ b/app/src/main/res/drawable/ic_sb_compare.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sb_edit.xml b/app/src/main/res/drawable/ic_sb_edit.xml new file mode 100644 index 0000000..e93574b --- /dev/null +++ b/app/src/main/res/drawable/ic_sb_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sb_logo.xml b/app/src/main/res/drawable/ic_sb_logo.xml new file mode 100644 index 0000000..a484598 --- /dev/null +++ b/app/src/main/res/drawable/ic_sb_logo.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_sb_publish.xml b/app/src/main/res/drawable/ic_sb_publish.xml new file mode 100644 index 0000000..de4e58d --- /dev/null +++ b/app/src/main/res/drawable/ic_sb_publish.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5668bbe --- /dev/null +++ b/app/src/main/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 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6754c23 --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.0.1" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c52ac9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..66f8458 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Aug 18 22:56:28 EEST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9a27a6c --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ]; do + ls=$(ls -ld "$PRG") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' >/dev/null; then + PRG="$link" + else + PRG=$(dirname "$PRG")"/$link" + fi +done +SAVED="$(pwd)" +cd "$(dirname \"$PRG\")/" >/dev/null +APP_HOME="$(pwd -P)" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=$(basename "$0") + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn() { + echo "$*" +} + +die() { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$(uname)" in +CYGWIN*) + cygwin=true + ;; +Darwin*) + darwin=true + ;; +MINGW*) + msys=true + ;; +NONSTOP*) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ]; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then + MAX_FD_LIMIT=$(ulimit -H -n) + if [ $? -eq 0 ]; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ]; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + APP_HOME=$(cygpath --path --mixed "$APP_HOME") + CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") + JAVACMD=$(cygpath --unix "$JAVACMD") + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) + SEP="" + for dir in $ROOTDIRSRAW; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ]; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@"; do + CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -) + CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition + eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") + else + eval $(echo args$i)="\"$arg\"" + fi + i=$((i + 1)) + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save() { + for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..20a37f5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "sb" \ No newline at end of file