refactor(youtube/sponsorblock): improve various implementations (#308)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
LisoUseInAIKyrios 2023-04-02 18:10:01 +04:00 committed by oSumAtrIX
parent 6528d444b4
commit e3529cfcec
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
60 changed files with 3432 additions and 3194 deletions

View File

@ -1,9 +1,7 @@
package app.revanced.integrations.patches;
import android.content.Context;
import android.widget.Toast;
import static app.revanced.integrations.utils.StringRef.str;
import app.revanced.integrations.sponsorblock.StringRef;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
@ -16,10 +14,8 @@ public class CopyVideoUrlPatch {
url += String.format("?t=%s", seconds);
}
Context context = ReVancedUtils.getContext();
ReVancedUtils.setClipboard(url);
if (context != null) Toast.makeText(context, StringRef.str("share_copy_url_success"), Toast.LENGTH_SHORT).show();
ReVancedUtils.showToastShort(str("share_copy_url_success"));
} catch (Exception e) {
LogHelper.printException(() -> "Failed to generate video url", e);
}

View File

@ -1,9 +1,6 @@
package app.revanced.integrations.patches;
import static app.revanced.integrations.utils.ReVancedUtils.getContext;
import android.content.Context;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.ThemeHelper;
public class LithoThemePatch {
@ -44,29 +41,15 @@ public class LithoThemePatch {
}
private static int getBlackColor() {
if (blackColor == 0) blackColor = getColor("yt_black1");
if (blackColor == 0) blackColor = ReVancedUtils.getResourceColor("yt_black1");
return blackColor;
}
private static int getWhiteColor() {
if (whiteColor == 0) whiteColor = getColor("yt_white1");
if (whiteColor == 0) whiteColor = ReVancedUtils.getResourceColor("yt_white1");
return whiteColor;
}
/**
* Determines the color for a color resource.
*
* @param name The color resource name.
* @return The value of the color.
*/
private static int getColor(String name) {
Context context = getContext();
return context != null ? context.getColor(context.getResources()
.getIdentifier(name, "color", context.getPackageName())
) : 0;
}
private static boolean anyEquals(int value, int... of) {
for (int v : of) if (value == v) return true;
return false;

View File

@ -1,16 +1,16 @@
package app.revanced.integrations.patches;
import static app.revanced.integrations.utils.StringRef.str;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.widget.Toast;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import java.util.Objects;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class MicroGSupport {
private static final String MICROG_VENDOR = "com.mgoogle";
@ -20,7 +20,7 @@ public class MicroGSupport {
private static final Uri VANCED_MICROG_PROVIDER = Uri.parse("content://" + MICROG_VENDOR + ".android.gsf.gservices/prefix");
private static void startIntent(Context context, String uriString, String message) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
ReVancedUtils.showToastLong(message);
var intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

View File

@ -1,7 +1,5 @@
package app.revanced.integrations.patches;
import android.widget.Toast;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper;
@ -83,13 +81,8 @@ public class SpoofSignatureVerificationPatch {
}
SettingsEnum.SIGNATURE_SPOOFING.saveValue(true);
ReVancedUtils.runOnMainThread(() -> {
Toast.makeText(
ReVancedUtils.getContext(),
"Spoofing app signature to prevent playback issues", Toast.LENGTH_LONG
).show();
ReVancedUtils.showToastLong("Spoofing app signature to prevent playback issues");
// it would be great if the video could be forcefully reloaded, but currently there is no code to do this
});
} catch (Exception ex) {
LogHelper.printException(() -> "onResponse failure", ex);

View File

@ -1,12 +1,12 @@
package app.revanced.integrations.patches;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/**
* Hooking class for the current playing video.
@ -17,19 +17,20 @@ public final class VideoInformation {
private static WeakReference<Object> playerController;
private static Method seekMethod;
@NonNull
private static String videoId = "";
private static long videoLength = 1;
private static long videoTime = -1;
private static long videoLength = 0;
private static volatile long videoTime = -1; // must be volatile. Value is set off main thread from high precision patch hook
/**
* Hook into PlayerController.onCreate() method.
* Injection point.
* Sets a reference to the YouTube playback controller.
*
* @param thisRef Reference to the player controller object.
*/
public static void playerController_onCreateHook(final Object thisRef) {
playerController = new WeakReference<>(thisRef);
videoLength = 1;
videoLength = 0;
videoTime = -1;
try {
@ -41,81 +42,115 @@ public final class VideoInformation {
}
/**
* Set the video id.
* Injection point.
*
* @param videoId The id of the video.
* @param newlyLoadedVideoId id of the current video
*/
public static void setVideoId(String videoId) {
LogHelper.printDebug(() -> "Setting current video id to: " + videoId);
VideoInformation.videoId = videoId;
public static void setVideoId(@NonNull String newlyLoadedVideoId) {
if (!videoId.equals(newlyLoadedVideoId)) {
LogHelper.printDebug(() -> "New video id: " + newlyLoadedVideoId);
videoId = newlyLoadedVideoId;
}
}
/**
* Set the video length.
* Injection point.
*
* @param length The length of the video in milliseconds.
*/
public static void setVideoLength(final long length) {
LogHelper.printDebug(() -> "Setting current video length to " + length);
if (videoLength != length) {
LogHelper.printDebug(() -> "Current video length: " + length);
videoLength = length;
}
}
/**
* Set the video time.
* Injection point.
* Called off the main thread approximately every 50ms to 100ms
*
* @param time The time of the video in milliseconds.
* @param currentPlaybackTime The current playback time of the video in milliseconds.
*/
public static void setVideoTime(final long time) {
LogHelper.printDebug(() -> "Current video time " + time);
videoTime = time;
public static void setVideoTimeHighPrecision(final long currentPlaybackTime) {
videoTime = currentPlaybackTime;
}
/**
* Seek on the current video.
* Does not function for playback of Shorts or Stories.
*
* Caution: If called from a videoTimeHook() callback,
* this will cause a recursive call into the same videoTimeHook() callback.
*
* @param millisecond The millisecond to seek the video to.
* @return if the seek was successful
*/
public static void seekTo(final long millisecond) {
new Handler(Looper.getMainLooper()).post(() -> {
public static boolean seekTo(final long millisecond) {
ReVancedUtils.verifyOnMainThread();
if (seekMethod == null) {
LogHelper.printDebug(() -> "seekMethod was null");
return;
LogHelper.printException(() -> "seekMethod was null");
return false;
}
try {
LogHelper.printDebug(() -> "Seeking to " + millisecond);
seekMethod.invoke(playerController.get(), millisecond);
return (Boolean) seekMethod.invoke(playerController.get(), millisecond);
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to seek", ex);
return false;
}
});
}
public static boolean seekToRelative(long millisecondsRelative) {
return seekTo(videoTime + millisecondsRelative);
}
/**
* Get the id of the current video playing.
* Id of the current video playing. Includes Shorts and YouTube Stories.
*
* @return The id of the video. Empty string if not set yet.
*/
@NonNull
public static String getCurrentVideoId() {
return videoId;
}
/**
* Get the length of the current video playing.
* Length of the current video playing.
* Includes Shorts playback.
*
* @return The length of the video in milliseconds. 1 if not set yet.
* @return The length of the video in milliseconds.
* If the video is not yet loaded, or if the video is playing in the background with no video visible,
* then this returns zero.
*/
public static long getCurrentVideoLength() {
return videoLength;
}
/**
* Get the time of the current video playing.
* Playback time of the current video playing.
* Value can lag up to approximately 100ms behind the actual current video playback time.
*
* Note: Code inside a videoTimeHook patch callback
* should use the callback video time and avoid using this method
* (in situations of recursive hook callbacks, the value returned here may be outdated).
*
* Includes Shorts playback.
*
* @return The time of the video in milliseconds. -1 if not set yet.
*/
public static long getVideoTime() {
return videoTime;
}
/**
* @return If the playback is at the end of the video.
*
* If video is playing in the background with no video visible,
* this always returns false (even if the video is actually at the end)
*/
public static boolean isAtEndOfVideo() {
return videoTime > 0 && videoLength > 0 && videoTime >= videoLength;
}
}

View File

@ -1,4 +0,0 @@
package app.revanced.integrations.patches.downloads.views
class DownloadOptions {
}

View File

@ -1,18 +1,18 @@
package app.revanced.integrations.patches.playback.quality;
import android.content.Context;
import android.net.ConnectivityManager;
import android.widget.Toast;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.ReVancedUtils.NetworkType;
import app.revanced.integrations.utils.SharedPrefHelper;
public class RememberVideoQualityPatch {
public static int selectedQuality1 = -2;
@ -22,12 +22,10 @@ public class RememberVideoQualityPatch {
public static void changeDefaultQuality(int defaultQuality) {
Context context = ReVancedUtils.getContext();
var networkType = getNetworType(context);
var networkType = ReVancedUtils.getNetworkType();
if (networkType == NetworkType.NONE) {
String message = "No internet connection.";
LogHelper.printDebug(() -> message);
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
ReVancedUtils.showToastShort("No internet connection.");
} else {
var preferenceKey = "wifi_quality";
var networkTypeMessage = "WIFI";
@ -38,8 +36,7 @@ public class RememberVideoQualityPatch {
}
SharedPrefHelper.saveString(SharedPrefHelper.SharedPrefNames.REVANCED_PREFS, preferenceKey, defaultQuality + "");
String message = "Changing default " + networkTypeMessage + " quality to: " + defaultQuality;
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
ReVancedUtils.showToastShort("Changing default " + networkTypeMessage + " quality to: " + defaultQuality);
}
userChangedQuality = false;
@ -89,7 +86,7 @@ public class RememberVideoQualityPatch {
LogHelper.printException(() -> "Context is null or settings not initialized, returning quality: " + qualityToLog);
return quality;
}
var networkType = getNetworType(context);
var networkType = ReVancedUtils.getNetworkType();
if (networkType == NetworkType.NONE) {
LogHelper.printDebug(() -> "No Internet connection!");
return quality;
@ -140,24 +137,4 @@ public class RememberVideoQualityPatch {
public static void newVideoStarted(String videoId) {
newVideo = true;
}
private static NetworkType getNetworType(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
var networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected()) {
return NetworkType.NONE;
} else {
var type = networkInfo.getType();
return type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_BLUETOOTH ? NetworkType.MOBILE : NetworkType.OTHER;
}
}
enum NetworkType {
MOBILE,
OTHER,
NONE
}
}

View File

@ -1,8 +1,8 @@
package app.revanced.integrations.patches.playback.speed;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
@ -19,10 +19,6 @@ public final class RememberPlaybackSpeedPatch {
@Nullable
private static String currentVideoId;
private static void showToast(final String message) {
Toast.makeText(ReVancedUtils.getContext(), message, Toast.LENGTH_LONG).show();
}
private static float getLastRememberedPlaybackSpeed() {
return SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_VALUE.getFloat();
}
@ -62,11 +58,11 @@ public final class RememberPlaybackSpeedPatch {
if (rememberLastSelectedPlaybackSpeed()) {
rememberPlaybackSpeed();
showToast("Remembering playback speed: " + playbackSpeed + "x");
ReVancedUtils.showToastLong("Remembering playback speed: " + playbackSpeed + "x");
} else {
if (getLastRememberedPlaybackSpeed() == DEFAULT_PLAYBACK_SPEED) return;
showToast("Applying playback speed: " + playbackSpeed + "x");
ReVancedUtils.showToastLong("Applying playback speed: " + playbackSpeed + "x");
}
}

View File

@ -1,6 +1,6 @@
package app.revanced.integrations.returnyoutubedislike;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import static app.revanced.integrations.utils.StringRef.str;
import android.graphics.Canvas;
import android.graphics.Paint;
@ -177,6 +177,11 @@ public class ReturnYouTubeDislike {
if (videoId.equals(currentVideoId)) {
return; // already loaded
}
if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id
LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId);
setCurrentVideoId(null);
return;
}
LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType);
setCurrentVideoId(videoId);
// no need to wrap the call in a try/catch,

View File

@ -1,12 +1,12 @@
package app.revanced.integrations.returnyoutubedislike.requests;
import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute;
import static app.revanced.integrations.utils.StringRef.str;
import android.util.Base64;
import android.widget.Toast;
import androidx.annotation.Nullable;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import org.json.JSONException;
import org.json.JSONObject;
@ -20,8 +20,10 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Objects;
import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class ReturnYouTubeDislikeApi {
/**
@ -192,9 +194,7 @@ public class ReturnYouTubeDislikeApi {
numberOfRateLimitRequestsEncountered++;
LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next "
+ RATE_LIMIT_BACKOFF_SECONDS + " seconds");
ReVancedUtils.runOnMainThread(() -> { // must show toasts on main thread
Toast.makeText(ReVancedUtils.getContext(), str("revanced_ryd_failure_client_rate_limit_requested"), Toast.LENGTH_LONG).show();
});
ReVancedUtils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested"));
return true;
}
return false;

View File

@ -1,6 +1,5 @@
package app.revanced.integrations.settings;
import android.content.Context;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
@ -131,20 +130,20 @@ public enum SettingsEnum {
// SponsorBlock settings
SB_ENABLED("sb-enabled", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_SHOW_TOAST_WHEN_SKIP("show-toast", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_COUNT_SKIPS("count-skips", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_VOTING_ENABLED("sb-voting-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_CREATE_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_USE_COMPACT_SKIPBUTTON("sb-use-compact-skip-button", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_SHOW_TOAST_ON_SKIP("show-toast", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_TRACK_SKIP_COUNT("count-skips", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_UUID("uuid", "", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING),
SB_ADJUST_NEW_SEGMENT_STEP("new-segment-step-accuracy", 150, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER),
SB_MIN_DURATION("sb-min-duration", 0F, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.FLOAT),
SB_SEEN_GUIDELINES("sb-seen-gl", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_NEW_SEGMENT_ENABLED("sb-new-segment-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_VOTING_ENABLED("sb-voting-enabled", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_SKIPPED_SEGMENTS("sb-skipped-segments", 0, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER),
SB_SKIPPED_SEGMENTS_TIME("sb-skipped-segments-time", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG),
SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED("sb-skipped-segments", 0, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.INTEGER),
SB_SKIPPED_SEGMENTS_TIME_SAVED("sb-skipped-segments-time", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG),
SB_SHOW_TIME_WITHOUT_SEGMENTS("sb-length-without-segments", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_IS_VIP("sb-is-vip", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_LAST_VIP_CHECK("sb-last-vip-check", 0L, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.LONG),
SB_SHOW_BROWSER_BUTTON("sb-browser-button", false, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
SB_API_URL("sb-api-host-url", "https://sponsor.ajay.app", SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.STRING);
private final String path;
@ -178,40 +177,40 @@ public enum SettingsEnum {
}
static {
load();
loadAllSettings();
}
private static void load() {
Context context = ReVancedUtils.getContext();
if (context == null) {
LogHelper.printException(() -> "SettingsEnum.load() called before ReVancedUtils.init()");
private static void loadAllSettings() {
if (ReVancedUtils.getContext() == null) {
LogHelper.printException(() -> "SettingsEnum loaded before ReVancedUtils context was set");
return;
}
for (SettingsEnum setting : values()) {
var path = setting.getPath();
var defaultValue = setting.getDefaultValue();
switch (setting.getReturnType()) {
setting.load();
}
}
private void load() {
switch (returnType) {
case FLOAT:
defaultValue = SharedPrefHelper.getFloat(setting.sharedPref, path, (float) defaultValue);
value = SharedPrefHelper.getFloat(sharedPref, path, (float) defaultValue);
break;
case LONG:
defaultValue = SharedPrefHelper.getLong(setting.sharedPref, path, (long) defaultValue);
value = SharedPrefHelper.getLong(sharedPref, path, (long) defaultValue);
break;
case BOOLEAN:
defaultValue = SharedPrefHelper.getBoolean(setting.sharedPref, path, (boolean) defaultValue);
value = SharedPrefHelper.getBoolean(sharedPref, path, (boolean) defaultValue);
break;
case INTEGER:
defaultValue = SharedPrefHelper.getInt(setting.sharedPref, path, (int) defaultValue);
value = SharedPrefHelper.getInt(sharedPref, path, (int) defaultValue);
break;
case STRING:
defaultValue = SharedPrefHelper.getString(setting.sharedPref, path, (String) defaultValue);
value = SharedPrefHelper.getString(sharedPref, path, (String) defaultValue);
break;
default:
LogHelper.printException(() -> "Setting does not have a valid Type. Name is: " + setting.name());
LogHelper.printException(() -> "Setting does not have a valid Type: " + name());
break;
}
setting.setValue(defaultValue);
}
}
/**
@ -227,14 +226,7 @@ public enum SettingsEnum {
* Sets the value, and persistently saves it
*/
public void saveValue(Object newValue) {
Context context = ReVancedUtils.getContext();
if (context == null) {
LogHelper.printException(() -> "Context on SaveValue is null!");
return;
}
switch (getReturnType()) {
switch (returnType) {
case FLOAT:
SharedPrefHelper.saveFloat(sharedPref, path, (float) newValue);
break;
@ -251,7 +243,7 @@ public enum SettingsEnum {
SharedPrefHelper.saveString(sharedPref, path, (String) newValue);
break;
default:
LogHelper.printException(() -> "Setting does not have a valid Type. Name is: " + name());
LogHelper.printException(() -> "Setting does not have a valid Type: " + name());
break;
}

View File

@ -1,6 +1,5 @@
package app.revanced.integrations.settingsmenu;
import android.content.Context;
import android.preference.PreferenceFragment;
import android.view.View;
import android.view.ViewGroup;
@ -24,11 +23,11 @@ public class ReVancedSettingActivity {
final var theme = ThemeHelper.isDarkTheme() ? darkTheme : whiteTheme;
LogHelper.printDebug(() -> "Using theme: " + theme);
base.setTheme(getIdentifier(theme, "style"));
base.setTheme(ReVancedUtils.getResourceIdentifier(theme, "style"));
}
public static void initializeSettings(LicenseActivity base) {
base.setContentView(getIdentifier("revanced_settings_with_toolbar", "layout"));
base.setContentView(ReVancedUtils.getResourceIdentifier("revanced_settings_with_toolbar", "layout"));
PreferenceFragment preferenceFragment;
String preferenceIdentifier;
@ -46,7 +45,7 @@ public class ReVancedSettingActivity {
}
try {
TextView toolbar = getTextView((ViewGroup) base.findViewById(getIdentifier("toolbar", "id")));
TextView toolbar = getTextView((ViewGroup) base.findViewById(ReVancedUtils.getResourceIdentifier("toolbar", "id")));
if (toolbar == null) {
// FIXME
// https://github.com/revanced/revanced-patches/issues/1384
@ -58,7 +57,7 @@ public class ReVancedSettingActivity {
LogHelper.printException(() -> "Could not set Toolbar title", e);
}
base.getFragmentManager().beginTransaction().replace(getIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit();
base.getFragmentManager().beginTransaction().replace(ReVancedUtils.getResourceIdentifier("revanced_settings_fragments", "id"), preferenceFragment).commit();
}
@ -86,10 +85,4 @@ public class ReVancedSettingActivity {
public static TextView getTextView(ViewGroup viewGroup) {
return getView(TextView.class, viewGroup);
}
private static int getIdentifier(String name, String defType) {
Context appContext = ReVancedUtils.getContext();
assert appContext != null;
return appContext.getResources().getIdentifier(name, defType, appContext.getPackageName());
}
}

View File

@ -1,5 +1,7 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlarmManager;
@ -8,11 +10,9 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Process;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
@ -86,8 +86,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
super.onCreate(bundle);
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.YOUTUBE.getName());
try {
int identifier = getResources().getIdentifier("revanced_prefs", "xml", getPackageName());
addPreferencesFromResource(identifier);
addPreferencesFromResource(ReVancedUtils.getResourceIdentifier("revanced_prefs", "xml"));
SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();
this.settingsInitialized = sharedPreferences.getBoolean("revanced_initialized", false);
@ -161,17 +160,7 @@ public class ReVancedSettingsFragment extends PreferenceFragment {
}
private void rebootDialog(final Activity activity) {
new AlertDialog.Builder(activity).setMessage(getStringByName(activity, "pref_refresh_config")).setPositiveButton(getStringByName(activity, "in_app_update_restart_button"), (dialog, id) -> reboot(activity, Shell_HomeActivity.class)).setNegativeButton(getStringByName(activity, "sign_in_cancel"), null).show();
}
private String getStringByName(Context context, String name) {
try {
Resources res = context.getResources();
return res.getString(res.getIdentifier(name, "string", context.getPackageName()));
} catch (Throwable exception) {
LogHelper.printException(() -> "Resource not found.", exception);
return "";
}
new AlertDialog.Builder(activity).setMessage(str("pref_refresh_config")).setPositiveButton(str("in_app_update_restart_button"), (dialog, id) -> reboot(activity, Shell_HomeActivity.class)).setNegativeButton(str("sign_in_cancel"), null).show();
}
}

View File

@ -1,6 +1,6 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.Activity;
import android.content.Intent;
@ -102,7 +102,7 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
// About category
PreferenceCategory aboutCategory = new PreferenceCategory(context);
aboutCategory.setTitle(str("about"));
aboutCategory.setTitle(str("revanced_ryd_about"));
preferenceScreen.addPreference(aboutCategory);
// ReturnYouTubeDislike Website

View File

@ -1,206 +1,373 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import static android.text.Html.fromHtml;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.util.Patterns;
import android.widget.EditText;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.DecimalFormat;
import java.util.ArrayList;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
import app.revanced.integrations.sponsorblock.objects.EditTextListPreference;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.sponsorblock.objects.SegmentCategoryListPreference;
import app.revanced.integrations.sponsorblock.objects.UserStats;
import app.revanced.integrations.sponsorblock.requests.SBRequester;
import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
public class SponsorBlockSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final DecimalFormat FORMATTER = new DecimalFormat("#,###,###");
public static final String SAVED_TEMPLATE = "%dh %.1f %s";
private static final APIURLChangeListener API_URL_CHANGE_LISTENER = new APIURLChangeListener();
private final ArrayList<Preference> preferencesToDisableWhenSBDisabled = new ArrayList<>();
@SuppressWarnings("deprecation")
public class SponsorBlockSettingsFragment extends PreferenceFragment {
private SwitchPreference sbEnabled;
private SwitchPreference addNewSegment;
private SwitchPreference votingEnabled;
private SwitchPreference compactSkipButton;
private SwitchPreference showSkipToast;
private SwitchPreference trackSkips;
private SwitchPreference showTimeWithoutSegments;
private EditTextPreference newSegmentStep;
private EditTextPreference minSegmentDuration;
private EditTextPreference privateUserId;
private EditTextPreference importExport;
private Preference apiUrl;
private PreferenceCategory statsCategory;
private PreferenceCategory segmentCategory;
private void updateUI() {
try {
final boolean enabled = SettingsEnum.SB_ENABLED.getBoolean();
if (!enabled) {
SponsorBlockViewController.hideSkipButton();
SponsorBlockViewController.hideNewSegmentLayout();
SegmentPlaybackController.setCurrentVideoId(null);
} else if (!SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()) {
SponsorBlockViewController.hideNewSegmentLayout();
}
// voting and add new segment buttons automatically shows/hides themselves
sbEnabled.setChecked(enabled);
addNewSegment.setChecked(SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean());
addNewSegment.setEnabled(enabled);
votingEnabled.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean());
votingEnabled.setEnabled(enabled);
compactSkipButton.setChecked(SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean());
compactSkipButton.setEnabled(enabled);
showSkipToast.setChecked(SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean());
showSkipToast.setEnabled(enabled);
trackSkips.setChecked(SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean());
trackSkips.setEnabled(enabled);
showTimeWithoutSegments.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
showTimeWithoutSegments.setEnabled(enabled);
newSegmentStep.setText(String.valueOf(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()));
newSegmentStep.setEnabled(enabled);
minSegmentDuration.setText(String.valueOf(SettingsEnum.SB_MIN_DURATION.getFloat()));
minSegmentDuration.setEnabled(enabled);
privateUserId.setText(SettingsEnum.SB_UUID.getString());
privateUserId.setEnabled(enabled);
apiUrl.setEnabled(enabled);
importExport.setEnabled(enabled);
segmentCategory.setEnabled(enabled);
statsCategory.setEnabled(enabled);
} catch (Exception ex) {
LogHelper.printException(() -> "update settings UI failure", ex);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK.getName());
try {
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setSharedPreferencesName(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK.getName());
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
final Activity context = this.getActivity();
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
Activity context = this.getActivity();
PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
SponsorBlockSettings.update(getActivity());
SponsorBlockSettings.initialize();
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(SettingsEnum.SB_ENABLED.getPath());
preference.setDefaultValue(SettingsEnum.SB_ENABLED.getDefaultValue());
preference.setChecked(SettingsEnum.SB_ENABLED.getBoolean());
preference.setTitle(str("enable_sb"));
preference.setSummary(str("enable_sb_sum"));
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
final boolean value = (Boolean) newValue;
enableCategoriesIfNeeded(value);
sbEnabled = new SwitchPreference(context);
sbEnabled.setTitle(str("sb_enable_sb"));
sbEnabled.setSummary(str("sb_enable_sb_sum"));
preferenceScreen.addPreference(sbEnabled);
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_ENABLED.saveValue(newValue);
updateUI();
return true;
});
}
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getPath());
preference.setDefaultValue(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getDefaultValue());
preference.setChecked(SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean());
preference.setTitle(str("enable_segmadding"));
preference.setSummary(str("enable_segmadding_sum"));
preferencesToDisableWhenSBDisabled.add(preference);
preference.setOnPreferenceChangeListener((preference12, o) -> {
final boolean value = (Boolean) o;
if (value && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) {
new AlertDialog.Builder(preference12.getContext())
addNewSegment = new SwitchPreference(context);
addNewSegment.setTitle(str("sb_enable_create_segment"));
addNewSegment.setSummaryOn(str("sb_enable_create_segment_sum_on"));
addNewSegment.setSummaryOff(str("sb_enable_create_segment_sum_off"));
preferenceScreen.addPreference(addNewSegment);
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
Boolean newValue = (Boolean) o;
if (newValue && !SettingsEnum.SB_SEEN_GUIDELINES.getBoolean()) {
SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true);
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("sb_guidelines_popup_title"))
.setMessage(str("sb_guidelines_popup_content"))
.setNegativeButton(str("sb_guidelines_popup_already_read"), null)
.setPositiveButton(str("sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
.show();
}
SettingsEnum.SB_NEW_SEGMENT_ENABLED.saveValue(value);
SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.saveValue(newValue);
updateUI();
return true;
});
}
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setTitle(str("enable_voting"));
preference.setSummary(str("enable_voting_sum"));
preference.setKey(SettingsEnum.SB_VOTING_ENABLED.getPath());
preference.setDefaultValue(SettingsEnum.SB_VOTING_ENABLED.getDefaultValue());
preference.setChecked(SettingsEnum.SB_VOTING_ENABLED.getBoolean());
preferencesToDisableWhenSBDisabled.add(preference);
preference.setOnPreferenceChangeListener((preference12, o) -> {
final boolean value = (Boolean) o;
SettingsEnum.SB_VOTING_ENABLED.saveValue(value);
votingEnabled = new SwitchPreference(context);
votingEnabled.setTitle(str("sb_enable_voting"));
votingEnabled.setSummaryOn(str("sb_enable_voting_sum_on"));
votingEnabled.setSummaryOff(str("sb_enable_voting_sum_off"));
preferenceScreen.addPreference(votingEnabled);
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_VOTING_ENABLED.saveValue(newValue);
updateUI();
return true;
});
compactSkipButton = new SwitchPreference(context);
compactSkipButton.setTitle(str("sb_enable_compact_skip_button"));
compactSkipButton.setSummaryOn(str("sb_enable_compact_skip_button_sum_on"));
compactSkipButton.setSummaryOff(str("sb_enable_compact_skip_button_sum_off"));
preferenceScreen.addPreference(compactSkipButton);
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.saveValue(newValue);
updateUI();
return true;
});
}
addGeneralCategory(context, preferenceScreen);
addSegmentsCategory(context, preferenceScreen);
addStatsCategory(context, preferenceScreen);
segmentCategory = new PreferenceCategory(context);
segmentCategory.setTitle(str("sb_diff_segments"));
preferenceScreen.addPreference(segmentCategory);
updateSegmentCategories();
statsCategory = new PreferenceCategory(context);
statsCategory.setTitle(str("sb_stats"));
preferenceScreen.addPreference(statsCategory);
fetchAndDisplayStats();
addAboutCategory(context, preferenceScreen);
enableCategoriesIfNeeded(SettingsEnum.SB_ENABLED.getBoolean());
updateUI();
} catch (Exception ex) {
LogHelper.printException(() -> "onCreate failure", ex);
}
}
private void openGuidelines() {
final Context context = getActivity();
SettingsEnum.SB_SEEN_GUIDELINES.saveValue(true);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
context.startActivity(intent);
}
private void enableCategoriesIfNeeded(boolean value) {
for (Preference preference : preferencesToDisableWhenSBDisabled)
preference.setEnabled(value);
}
@Override
public void onDestroy() {
super.onDestroy();
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
private void addSegmentsCategory(Context context, PreferenceScreen screen) {
PreferenceCategory category = new PreferenceCategory(context);
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
final PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
preferencesToDisableWhenSBDisabled.add(category);
category.setTitle(str("diff_segments"));
category.setTitle(str("sb_general"));
SponsorBlockSettings.SegmentBehaviour[] segmentBehaviours = SponsorBlockSettings.SegmentBehaviour.values();
String[] entries = new String[segmentBehaviours.length];
String[] entryValues = new String[segmentBehaviours.length];
for (int i = 0, segmentBehavioursLength = segmentBehaviours.length; i < segmentBehavioursLength; i++) {
SponsorBlockSettings.SegmentBehaviour behaviour = segmentBehaviours[i];
entries[i] = behaviour.name.toString();
entryValues[i] = behaviour.key;
Preference guidelinePreferences = new Preference(context);
guidelinePreferences.setTitle(str("sb_guidelines_preference_title"));
guidelinePreferences.setSummary(str("sb_guidelines_preference_sum"));
guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
openGuidelines();
return true;
});
category.addPreference(guidelinePreferences);
showSkipToast = new SwitchPreference(context);
showSkipToast.setTitle(str("sb_general_skiptoast"));
showSkipToast.setSummaryOn(str("sb_general_skiptoast_sum_on"));
showSkipToast.setSummaryOff(str("sb_general_skiptoast_sum_off"));
showSkipToast.setOnPreferenceClickListener(preference1 -> {
ReVancedUtils.showToastShort(str("sb_skipped_sponsor"));
return false;
});
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(newValue);
updateUI();
return true;
});
category.addPreference(showSkipToast);
trackSkips = new SwitchPreference(context);
trackSkips.setTitle(str("sb_general_skipcount"));
trackSkips.setSummaryOn(str("sb_general_skipcount_sum_on"));
trackSkips.setSummaryOff(str("sb_general_skipcount_sum_off"));
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(newValue);
updateUI();
return true;
});
category.addPreference(trackSkips);
showTimeWithoutSegments = new SwitchPreference(context);
showTimeWithoutSegments.setTitle(str("sb_general_time_without"));
showTimeWithoutSegments.setSummaryOn(str("sb_general_time_without_sum_on"));
showTimeWithoutSegments.setSummaryOff(str("sb_general_time_without_sum_off"));
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue);
updateUI();
return true;
});
category.addPreference(showTimeWithoutSegments);
newSegmentStep = new EditTextPreference(context);
newSegmentStep.setTitle(str("sb_general_adjusting"));
newSegmentStep.setSummary(str("sb_general_adjusting_sum"));
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
if (newAdjustmentValue == 0) {
ReVancedUtils.showToastLong(str("sb_general_adjusting_invalid"));
return false;
}
SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(newAdjustmentValue);
return true;
});
category.addPreference(newSegmentStep);
minSegmentDuration = new EditTextPreference(context);
minSegmentDuration.setTitle(str("sb_general_min_duration"));
minSegmentDuration.setSummary(str("sb_general_min_duration_sum"));
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString()));
return true;
});
category.addPreference(minSegmentDuration);
privateUserId = new EditTextPreference(context);
privateUserId.setTitle(str("sb_general_uuid"));
privateUserId.setSummary(str("sb_general_uuid_sum"));
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
String newUUID = newValue.toString();
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
ReVancedUtils.showToastLong(str("sb_general_uuid_invalid"));
return false;
}
SettingsEnum.SB_UUID.saveValue(newUUID);
fetchAndDisplayStats();
return true;
});
category.addPreference(privateUserId);
apiUrl = new Preference(context);
apiUrl.setTitle(str("sb_general_api_url"));
apiUrl.setSummary(Html.fromHtml(str("sb_general_api_url_sum")));
apiUrl.setOnPreferenceClickListener(preference1 -> {
EditText editText = new EditText(context);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
editText.setText(SettingsEnum.SB_API_URL.getString());
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.getDefaultValue());
ReVancedUtils.showToastLong(str("sb_api_url_reset"));
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
String serverAddress = editText.getText().toString();
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
ReVancedUtils.showToastLong(str("sb_api_url_invalid"));
} else if (!serverAddress.equals(SettingsEnum.SB_API_URL.getString())) {
SettingsEnum.SB_API_URL.saveValue(serverAddress);
ReVancedUtils.showToastLong(str("sb_api_url_changed"));
}
}
};
new AlertDialog.Builder(context)
.setTitle(apiUrl.getTitle())
.setView(editText)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("sb_reset"), urlChangeListener)
.setPositiveButton(android.R.string.ok, urlChangeListener)
.show();
return true;
});
category.addPreference(apiUrl);
importExport = new EditTextPreference(context);
importExport.setTitle(str("sb_settings_ie"));
importExport.setSummary(str("sb_settings_ie_sum"));
importExport.setOnPreferenceClickListener(preference1 -> {
importExport.getEditText().setText(SponsorBlockSettings.exportSettings());
return true;
});
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
SponsorBlockSettings.importSettings((String) newValue);
updateSegmentCategories();
fetchAndDisplayStats();
updateUI();
return true;
});
category.addPreference(importExport);
}
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
private void updateSegmentCategories() {
try {
segmentCategory.removeAll();
for (SponsorBlockSettings.SegmentInfo segmentInfo : categories) {
EditTextListPreference preference = new EditTextListPreference(context);
preference.setTitle(segmentInfo.getTitleWithDot());
preference.setSummary(segmentInfo.description.toString());
preference.setKey(segmentInfo.key);
preference.setDefaultValue(segmentInfo.behaviour.key);
preference.setEntries(entries);
preference.setEntryValues(entryValues);
category.addPreference(preference);
Activity activity = getActivity();
for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) {
segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category));
}
Preference colorPreference = new Preference(context); // TODO remove this after the next major update
screen.addPreference(colorPreference);
colorPreference.setTitle(str("color_change"));
colorPreference.setSummary(str("color_change_sum"));
colorPreference.setSelectable(false);
preferencesToDisableWhenSBDisabled.add(colorPreference);
}
private void addStatsCategory(Context context, PreferenceScreen screen) {
PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
category.setTitle(str("stats"));
preferencesToDisableWhenSBDisabled.add(category);
{
Preference preference = new Preference(context);
category.addPreference(preference);
preference.setTitle(str("stats_loading"));
preference.setSelectable(false);
SBRequester.retrieveUserStats(category, preference);
} catch (Exception ex) {
LogHelper.printException(() -> "updateSegmentCategories failure", ex);
}
}
private void addAboutCategory(Context context, PreferenceScreen screen) {
PreferenceCategory category = new PreferenceCategory(context);
screen.addPreference(category);
category.setTitle(str("about"));
category.setTitle(str("sb_about"));
{
Preference preference = new Preference(context);
screen.addPreference(preference);
preference.setTitle(str("about_api"));
preference.setSummary(str("about_api_sum"));
preference.setTitle(str("sb_about_api"));
preference.setSummary(str("sb_about_api_sum"));
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sponsor.ajay.app"));
@ -212,197 +379,160 @@ public class SponsorBlockSettingsFragment extends PreferenceFragment implements
{
Preference preference = new Preference(context);
screen.addPreference(preference);
preference.setTitle(str("about_madeby"));
preference.setSummary(str("sb_about_made_by"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
preference.setSingleLineTitle(false);
}
preference.setSelectable(false);
}
}
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
final PreferenceCategory category = new PreferenceCategory(context);
preferencesToDisableWhenSBDisabled.add(category);
screen.addPreference(category);
category.setTitle(str("general"));
private void openGuidelines() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
getActivity().startActivity(intent);
}
{
Preference preference = new Preference(context);
preference.setTitle(str("sb_guidelines_preference_title"));
preference.setSummary(str("sb_guidelines_preference_sum"));
preference.setOnPreferenceClickListener(preference1 -> {
openGuidelines();
return false;
private void fetchAndDisplayStats() {
try {
statsCategory.removeAll();
Preference loadingPlaceholderPreference = new Preference(this.getActivity());
loadingPlaceholderPreference.setEnabled(false);
statsCategory.addPreference(loadingPlaceholderPreference);
if (SettingsEnum.SB_ENABLED.getBoolean()) {
loadingPlaceholderPreference.setTitle(str("sb_stats_loading"));
ReVancedUtils.runOnBackgroundThread(() -> {
UserStats stats = SBRequester.retrieveUserStats();
ReVancedUtils.runOnMainThread(() -> { // get back on main thread to modify UI elements
addUserStats(loadingPlaceholderPreference, stats);
});
screen.addPreference(preference);
}
{
SwitchPreference preference = new SwitchPreference(context);
preference.setTitle(str("general_skiptoast"));
preference.setSummary(str("general_skiptoast_sum"));
preference.setChecked(SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean());
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.saveValue(newValue);
return true;
});
preference.setOnPreferenceClickListener(preference12 -> {
Toast.makeText(preference12.getContext(), str("skipped_sponsor"), Toast.LENGTH_SHORT).show();
return false;
});
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
} else {
loadingPlaceholderPreference.setTitle(str("sb_stats_sb_disabled"));
}
{
SwitchPreference preference = new SwitchPreference(context);
preference.setTitle(str("general_skipcount"));
preference.setSummary(str("general_skipcount_sum"));
preference.setChecked(SettingsEnum.SB_COUNT_SKIPS.getBoolean());
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_COUNT_SKIPS.saveValue(newValue);
return true;
});
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
SwitchPreference preference = new SwitchPreference(context);
preference.setTitle(str("general_time_without_sb"));
preference.setSummary(str("general_time_without_sb_sum"));
preference.setChecked(SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(newValue);
return true;
});
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
preference.setTitle(str("general_adjusting"));
preference.setSummary(str("general_adjusting_sum"));
preference.setText(String.valueOf(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt()));
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.saveValue(Integer.valueOf(newValue.toString()));
return true;
});
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
preference.setTitle(str("general_min_duration"));
preference.setSummary(str("general_min_duration_sum"));
preference.setText(String.valueOf(SettingsEnum.SB_MIN_DURATION.getFloat()));
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(newValue.toString()));
return true;
});
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.setTitle(str("general_uuid"));
preference.setSummary(str("general_uuid_sum"));
preference.setText(SettingsEnum.SB_UUID.getString());
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SettingsEnum.SB_UUID.saveValue(newValue.toString());
return true;
});
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
Preference preference = new Preference(context);
String title = str("general_api_url");
preference.setTitle(title);
preference.setSummary(Html.fromHtml(str("general_api_url_sum")));
preference.setOnPreferenceClickListener(preference1 -> {
EditText editText = new EditText(context);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
editText.setText(SettingsEnum.SB_API_URL.getString());
API_URL_CHANGE_LISTENER.setEditTextRef(editText);
new AlertDialog.Builder(context)
.setTitle(title)
.setView(editText)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("reset"), API_URL_CHANGE_LISTENER)
.setPositiveButton(android.R.string.ok, API_URL_CHANGE_LISTENER)
.show();
return true;
});
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
Context applicationContext = context.getApplicationContext();
preference.setTitle(str("settings_ie"));
preference.setSummary(str("settings_ie_sum"));
preference.setText(SponsorBlockUtils.exportSettings(applicationContext));
preference.setOnPreferenceChangeListener((preference1, newValue) -> {
SponsorBlockUtils.importSettings((String) newValue, applicationContext);
return false;
});
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
} catch (Exception ex) {
LogHelper.printException(() -> "fetchAndDisplayStats failure", ex);
}
}
private static class APIURLChangeListener implements DialogInterface.OnClickListener {
private WeakReference<EditText> editTextRef;
private static final DecimalFormat statsNumberOfSegmentsSkippedFormatter = new DecimalFormat("#,###,###");
@Override
public void onClick(DialogInterface dialog, int which) {
EditText editText = editTextRef.get();
if (editText == null)
private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) {
ReVancedUtils.verifyOnMainThread();
try {
if (stats == null) {
loadingPlaceholder.setTitle(str("sb_stats_connection_failure"));
return;
Context context = ((AlertDialog) dialog).getContext();
Context applicationContext = context.getApplicationContext();
}
statsCategory.removeAll();
Context context = statsCategory.getContext();
switch (which) {
case DialogInterface.BUTTON_NEUTRAL:
SettingsEnum.SB_API_URL.saveValue(SettingsEnum.SB_API_URL.getDefaultValue());
Toast.makeText(applicationContext, str("api_url_reset"), Toast.LENGTH_SHORT).show();
break;
case DialogInterface.BUTTON_POSITIVE:
Editable text = editText.getText();
Toast invalidToast = Toast.makeText(applicationContext, str("api_url_invalid"), Toast.LENGTH_SHORT);
if (text == null) {
invalidToast.show();
{
EditTextPreference preference = new EditTextPreference(context);
statsCategory.addPreference(preference);
String userName = stats.userName;
preference.setTitle(fromHtml(str("sb_stats_username", userName)));
preference.setSummary(str("sb_stats_username_change"));
preference.setText(userName);
preference.setOnPreferenceChangeListener((preference1, value) -> {
ReVancedUtils.runOnBackgroundThread(() -> {
String newUserName = (String) value;
String errorMessage = SBRequester.setUsername(newUserName);
ReVancedUtils.runOnMainThread(() -> {
if (errorMessage == null) {
preference.setTitle(fromHtml(str("sb_stats_username", newUserName)));
preference.setText(newUserName);
ReVancedUtils.showToastLong(str("sb_stats_username_changed"));
} else {
String textAsString = text.toString();
if (textAsString.isEmpty() || !Patterns.WEB_URL.matcher(textAsString).matches()) {
invalidToast.show();
preference.setText(userName); // revert to previous
ReVancedUtils.showToastLong(errorMessage);
}
});
});
return true;
});
}
{
// number of segment submissions (does not include ignored segments)
Preference preference = new Preference(context);
statsCategory.addPreference(preference);
String formatted = statsNumberOfSegmentsSkippedFormatter.format(stats.segmentCount);
preference.setTitle(fromHtml(str("sb_stats_submissions", formatted)));
if (stats.segmentCount == 0) {
preference.setSelectable(false);
} else {
SettingsEnum.SB_API_URL.saveValue(textAsString);
Toast.makeText(applicationContext, str("api_url_changed"), Toast.LENGTH_SHORT).show();
}
}
break;
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
preference1.getContext().startActivity(i);
return true;
});
}
}
public void setEditTextRef(EditText editText) {
editTextRef = new WeakReference<>(editText);
{
// "user reputation". Usually not useful, since it appears most users have zero reputation.
// But if there is a reputation, then show it here
Preference preference = new Preference(context);
preference.setTitle(fromHtml(str("sb_stats_reputation", stats.reputation)));
preference.setSelectable(false);
if (stats.reputation != 0) {
statsCategory.addPreference(preference);
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
SponsorBlockSettings.update(getActivity());
{
// time saved for other users
Preference preference = new Preference(context);
statsCategory.addPreference(preference);
String stats_saved;
String stats_saved_sum;
if (stats.segmentCount == 0) {
stats_saved = str("sb_stats_saved_zero");
stats_saved_sum = str("sb_stats_saved_sum_zero");
} else {
stats_saved = str("sb_stats_saved", statsNumberOfSegmentsSkippedFormatter.format(stats.viewCount));
stats_saved_sum = str("sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
}
preference.setTitle(fromHtml(stats_saved));
preference.setSummary(fromHtml(stats_saved_sum));
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
preference1.getContext().startActivity(i);
return false;
});
}
{
// time the user saved by using SB
Preference preference = new Preference(context);
statsCategory.addPreference(preference);
Runnable updateStatsSelfSaved = () -> {
String formatted = statsNumberOfSegmentsSkippedFormatter.format(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt());
preference.setTitle(fromHtml(str("sb_stats_self_saved", formatted)));
String formattedSaved = SponsorBlockUtils.getTimeSavedString(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / 1000);
preference.setSummary(fromHtml(str("sb_stats_self_saved_sum", formattedSaved)));
};
updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> {
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("sb_stats_self_saved_reset_title"))
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getDefaultValue());
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getDefaultValue());
updateStatsSelfSaved.run();
})
.setNegativeButton(android.R.string.no, null).show();
return true;
});
}
} catch (Exception ex) {
LogHelper.printException(() -> "fetchAndDisplayStats failure", ex);
}
}
}

View File

@ -19,13 +19,13 @@ class PlayerControlsVisibilityObserverImpl(
* id of the direct parent of controls_layout, R.id.youtube_controls_overlay
*/
private val controlsLayoutParentId =
ReVancedUtils.getResourceIdByName(activity, "id", "youtube_controls_overlay")
ReVancedUtils.getResourceIdentifier(activity, "youtube_controls_overlay", "id")
/**
* id of R.id.controls_layout
*/
private val controlsLayoutId =
ReVancedUtils.getResourceIdByName(activity, "id", "controls_layout")
ReVancedUtils.getResourceIdentifier(activity, "controls_layout", "id")
/**
* reference to the controls layout view

View File

@ -7,8 +7,8 @@ import app.revanced.integrations.utils.Event
*/
@Suppress("unused")
enum class PlayerType {
NONE, // includes Shorts playback
HIDDEN, // also includes YouTube Shorts and Stories, if a regular video is minimized and a Short/Story is then opened
NONE, // includes Shorts and Stories playback
HIDDEN, // A Shorts or Stories, if a regular video is minimized and a Short/Story is then opened
WATCH_WHILE_MINIMIZED,
WATCH_WHILE_MAXIMIZED,
WATCH_WHILE_FULLSCREEN,
@ -48,6 +48,7 @@ enum class PlayerType {
/**
* player type change listener
*/
@JvmStatic
val onChange = Event<PlayerType>()
}

View File

@ -1,97 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.view.View;
import android.view.ViewGroup;
import java.lang.reflect.Field;
import app.revanced.integrations.utils.LogHelper;
// invoke-static {p0}, Lpl/jakubweg/InjectedPlugin;->inject(Landroid/content/Context;)V
// invoke-static {}, Lpl/jakubweg/InjectedPlugin;->printSomething()V
// InlineTimeBar
public class InjectedPlugin {
public static void printSomething() {
LogHelper.printDebug(() -> "printSomething called");
}
public static void printObject(Object o, int recursive) {
if (o == null)
LogHelper.printDebug(() -> "Printed object is null");
else {
LogHelper.printDebug(() -> "Printed object ("
+ o.getClass().getName()
+ ") = " + o.toString());
for (Field field : o.getClass().getDeclaredFields()) {
if (field.getType().isPrimitive())
continue;
field.setAccessible(true);
try {
Object value = field.get(o);
try {
// if ("java.lang.String".equals(field.getType().getName()))
LogHelper.printDebug(() -> "Field: " + field.toString() + " has value " + value);
} catch (Exception e) {
LogHelper.printDebug(() -> "Field: " + field.toString() + " has value that thrown an exception in toString method");
}
if (recursive > 0 && value != null && !value.getClass().isPrimitive())
printObject(value, recursive - 1);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
public static void printObject(Object o) {
printObject(o, 0);
}
public static void printObject(int o) {
printObject(Integer.valueOf(o));
}
public static void printObject(float o) {
printObject(Float.valueOf(o));
}
public static void printObject(long o) {
printObject(Long.valueOf(o));
}
public static void printStackTrace() {
StackTraceElement[] stackTrace = (new Throwable()).getStackTrace();
LogHelper.printDebug(() -> "Printing stack trace:");
for (StackTraceElement element : stackTrace) {
LogHelper.printDebug(() -> element.toString());
}
}
public static void printViewStack(final View view, int spaces) {
StringBuilder builder = new StringBuilder(spaces);
for (int i = 0; i < spaces; i++) {
builder.append('-');
}
String spacesStr = builder.toString();
if (view == null) {
LogHelper.printDebug(() -> spacesStr + "Null view");
return;
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
LogHelper.printDebug(() -> spacesStr + "View group: " + view);
int childCount = group.getChildCount();
LogHelper.printDebug(() -> spacesStr + "Children count: " + childCount);
for (int i = 0; i < childCount; i++) {
printViewStack(group.getChildAt(i), spaces + 1);
}
} else {
LogHelper.printDebug(() -> spacesStr + "Normal view: " + view);
}
}
}

View File

@ -1,28 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.content.Context;
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.hideNewSegmentLayout;
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.showNewSegmentLayout;
public class NewSegmentHelperLayout {
public static Context context;
private static boolean isShown = false;
public static void show() {
if (isShown) return;
isShown = true;
showNewSegmentLayout();
}
public static void hide() {
if (!isShown) return;
isShown = false;
hideNewSegmentLayout();
}
public static void toggle() {
if (isShown) hide();
else show();
}
}

View File

@ -1,431 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.sponsorblock.requests.SBRequester;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.timeWithoutSegments;
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.videoHasSegments;
public class PlayerController {
private static final Timer sponsorTimer = new Timer("sponsor-skip-timer");
public static WeakReference<Activity> playerActivity = new WeakReference<>(null);
public static SponsorSegment[] sponsorSegmentsOfCurrentVideo;
private static long allowNextSkipRequestTime = 0L;
private static String currentVideoId;
private static long lastKnownVideoTime = -1L;
private static final Runnable findAndSkipSegmentRunnable = () -> {
findAndSkipSegment(false);
};
private static float sponsorBarLeft = 1f;
private static float sponsorBarRight = 1f;
private static float sponsorBarThickness = 2f;
private static TimerTask skipSponsorTask = null;
public static String getCurrentVideoId() {
return currentVideoId;
}
public static void setCurrentVideoId(final String videoId) {
try {
if (videoId == null) {
currentVideoId = null;
sponsorSegmentsOfCurrentVideo = null;
return;
}
// currently this runs every time a video is loaded (regardless if sponsorblock is turned on or off)
// FIXME: change this so if sponsorblock is disabled, then run this method exactly once and once only
SponsorBlockSettings.update(null);
if (!SettingsEnum.SB_ENABLED.getBoolean()) {
currentVideoId = null;
return;
}
if (PlayerType.getCurrent() == PlayerType.NONE) {
LogHelper.printDebug(() -> "ignoring shorts video");
currentVideoId = null;
return;
}
if (videoId.equals(currentVideoId))
return;
currentVideoId = videoId;
sponsorSegmentsOfCurrentVideo = null;
LogHelper.printDebug(() -> "setCurrentVideoId: videoId=" + videoId);
sponsorTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
executeDownloadSegments(currentVideoId);
} catch (Exception e) {
LogHelper.printException(() -> "Failed to download segments", e);
}
}
}, 0);
} catch (Exception ex) {
LogHelper.printException(() -> "setCurrentVideoId failure", ex);
}
}
/**
* Called when creating some kind of youtube internal player controlled, every time when new video starts to play
*/
public static void initialize(Object _o) {
try {
lastKnownVideoTime = 0;
SkipSegmentView.hide();
NewSegmentHelperLayout.hide();
} catch (Exception ex) {
LogHelper.printException(() -> "initialize failure", ex);
}
}
public static void executeDownloadSegments(String videoId) {
try {
videoHasSegments = false;
timeWithoutSegments = "";
SponsorSegment[] segments = SBRequester.getSegments(videoId);
Arrays.sort(segments);
for (SponsorSegment segment : segments) {
LogHelper.printDebug(() -> "Detected segment: " + segment.toString());
}
sponsorSegmentsOfCurrentVideo = segments;
// new Handler(Looper.getMainLooper()).post(findAndSkipSegmentRunnable);
} catch (Exception ex) {
LogHelper.printException(() -> "executeDownloadSegments failure", ex);
}
}
public static void setVideoTime(long millis) {
try {
if (!SettingsEnum.SB_ENABLED.getBoolean()) return;
LogHelper.printDebug(() -> "setCurrentVideoTime: current video time: " + millis);
// fixme? if (millis == lastKnownVideoTime), should it return here and not continue?
lastKnownVideoTime = millis;
if (millis <= 0) return;
//findAndSkipSegment(false);
if (millis == VideoInformation.getCurrentVideoLength()) {
SponsorBlockUtils.hideShieldButton();
SponsorBlockUtils.hideVoteButton();
return;
}
SponsorSegment[] segments = sponsorSegmentsOfCurrentVideo;
if (segments == null || segments.length == 0) return;
final long START_TIMER_BEFORE_SEGMENT_MILLIS = 1200;
final long startTimerAtMillis = millis + START_TIMER_BEFORE_SEGMENT_MILLIS;
for (final SponsorSegment segment : segments) {
if (segment.start > millis) {
if (segment.start > startTimerAtMillis)
break; // it's more then START_TIMER_BEFORE_SEGMENT_MILLIS far away
if (!segment.category.behaviour.skip)
break;
if (skipSponsorTask == null) {
LogHelper.printDebug(() -> "Scheduling skipSponsorTask");
skipSponsorTask = new TimerTask() {
@Override
public void run() {
skipSponsorTask = null;
lastKnownVideoTime = segment.start + 1;
ReVancedUtils.runOnMainThread(findAndSkipSegmentRunnable);
}
};
sponsorTimer.schedule(skipSponsorTask, segment.start - millis);
} else {
LogHelper.printDebug(() -> "skipSponsorTask is already scheduled...");
}
break;
}
if (segment.end < millis)
continue;
// we are in the segment!
if (segment.category.behaviour.skip && !(segment.category.behaviour.key.equals("skip-once") && segment.didAutoSkipped)) {
sendViewRequestAsync(millis, segment);
skipSegment(segment, false);
break;
} else {
SkipSegmentView.show();
return;
}
}
SkipSegmentView.hide();
} catch (Exception e) {
LogHelper.printException(() -> "setVideoTime failure", e);
}
}
private static void sendViewRequestAsync(final long millis, final SponsorSegment segment) {
if (segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED) {
Context context = ReVancedUtils.getContext();
if (context != null) {
long newSkippedTime = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() + (segment.end - segment.start);
SettingsEnum.SB_SKIPPED_SEGMENTS.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS.getInt() + 1);
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.saveValue(newSkippedTime);
}
}
if (SettingsEnum.SB_COUNT_SKIPS.getBoolean()
&& segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED
&& millis - segment.start < 2000) { // Only skips from the start should count as a view
ReVancedUtils.runOnBackgroundThread(() -> {
SBRequester.sendViewCountRequest(segment);
});
}
}
public static void setHighPrecisionVideoTime(final long millis) {
try {
if ((millis < lastKnownVideoTime && lastKnownVideoTime >= VideoInformation.getCurrentVideoLength()) || millis == 0) {
SponsorBlockUtils.showShieldButton(); // skipping from end to the video will show the buttons again
SponsorBlockUtils.showVoteButton();
}
if (lastKnownVideoTime > 0) {
lastKnownVideoTime = millis;
} else
setVideoTime(millis);
} catch (Exception ex) {
LogHelper.printException(() -> "setHighPrecisionVideoTime failure", ex);
}
}
public static long getCurrentVideoLength() {
return VideoInformation.getCurrentVideoLength();
}
public static long getLastKnownVideoTime() {
return lastKnownVideoTime;
}
public static void setSponsorBarAbsoluteLeft(final Rect rect) {
setSponsorBarAbsoluteLeft(rect.left);
}
public static void setSponsorBarAbsoluteLeft(final float left) {
LogHelper.printDebug(() -> String.format("setSponsorBarLeft: left=%.2f", left));
sponsorBarLeft = left;
}
public static void setSponsorBarRect(final Object self) {
try {
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
field.setAccessible(true);
Rect rect = (Rect) field.get(self);
if (rect != null) {
setSponsorBarAbsoluteLeft(rect.left);
setSponsorBarAbsoluteRight(rect.right);
}
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarRect failure", ex);
}
}
public static void setSponsorBarAbsoluteRight(final Rect rect) {
setSponsorBarAbsoluteRight(rect.right);
}
public static void setSponsorBarAbsoluteRight(final float right) {
LogHelper.printDebug(() -> String.format("setSponsorBarRight: right=%.2f", right));
sponsorBarRight = right;
}
public static void setSponsorBarThickness(final int thickness) {
try {
setSponsorBarThickness((float) thickness);
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarThickness failure", ex);
}
}
public static void setSponsorBarThickness(final float thickness) {
// if (VERBOSE_DRAW_OPTIONS)
// LogH(PlayerController.class, String.format("setSponsorBarThickness: thickness=%.2f", thickness));
sponsorBarThickness = thickness;
}
public static void onSkipSponsorClicked() {
LogHelper.printDebug(() -> "Skip segment clicked");
findAndSkipSegment(true);
}
public static void addSkipSponsorView15(final View view) {
try {
playerActivity = new WeakReference<>((Activity) view.getContext());
LogHelper.printDebug(() -> "addSkipSponsorView15: view=" + view.toString());
ReVancedUtils.runOnMainThreadDelayed(() -> {
final ViewGroup viewGroup = (ViewGroup) ((ViewGroup) view).getChildAt(2);
Activity context = ((Activity) viewGroup.getContext());
NewSegmentHelperLayout.context = context;
}, 500);
} catch (Exception ex) {
LogHelper.printException(() -> "addSkipSponsorView15 failure", ex);
}
}
// Edit: Is this method ever called? Where is the patch code that calls this?
public static void addSkipSponsorView14(final View view) {
try {
playerActivity = new WeakReference<>((Activity) view.getContext());
LogHelper.printDebug(() -> "addSkipSponsorView14: view=" + view.toString());
ReVancedUtils.runOnMainThreadDelayed(() -> {
final ViewGroup viewGroup = (ViewGroup) view.getParent();
Activity activity = (Activity) viewGroup.getContext();
NewSegmentHelperLayout.context = activity;
}, 500);
} catch (Exception ex) {
LogHelper.printException(() -> "addSkipSponsorView14 failure", ex);
}
}
/**
* Called when it's time to draw time bar
*/
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
try {
if (sponsorBarThickness < 0.1) return;
if (sponsorSegmentsOfCurrentVideo == null) return;
final float thicknessDiv2 = sponsorBarThickness / 2;
final float top = posY - thicknessDiv2;
final float bottom = posY + thicknessDiv2;
final float absoluteLeft = sponsorBarLeft;
final float absoluteRight = sponsorBarRight;
final float tmp1 = 1f / (float) VideoInformation.getCurrentVideoLength() * (absoluteRight - absoluteLeft);
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
float left = segment.start * tmp1 + absoluteLeft;
float right = segment.end * tmp1 + absoluteLeft;
canvas.drawRect(left, top, right, bottom, segment.category.paint);
}
} catch (Exception ex) {
LogHelper.printException(() -> "drawSponsorTimeBars failure", ex);
}
}
// private final static Pattern videoIdRegex = Pattern.compile(".*\\.be\\/([A-Za-z0-9_\\-]{0,50}).*");
public static String substringVideoIdFromLink(String link) {
return link.substring(link.lastIndexOf('/') + 1);
}
public static void skipRelativeMilliseconds(int millisRelative) {
skipToMillisecond(lastKnownVideoTime + millisRelative);
}
public static boolean skipToMillisecond(long millisecond) {
// in 15.x if sponsor clip hits the end, then it crashes the app, because of too many function invocations
// I put this block so that skip can be made only once per some time
long now = System.currentTimeMillis();
if (now < allowNextSkipRequestTime) {
LogHelper.printDebug(() -> "skipToMillisecond: to fast, slow down, because you'll fail");
return false;
}
allowNextSkipRequestTime = now + 100;
LogHelper.printDebug(() -> String.format("Requesting skip to millis=%d on thread %s", millisecond, Thread.currentThread().toString()));
final long finalMillisecond = millisecond;
try {
LogHelper.printDebug(() -> "Skipping to millis=" + finalMillisecond);
lastKnownVideoTime = finalMillisecond;
VideoInformation.seekTo(finalMillisecond);
} catch (Exception e) {
LogHelper.printException(() -> "Cannot skip to millisecond", e);
}
return true;
}
private static void findAndSkipSegment(boolean wasClicked) {
try {
if (sponsorSegmentsOfCurrentVideo == null)
return;
final long millis = lastKnownVideoTime;
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
if (segment.start > millis)
break;
if (segment.end < millis)
continue;
SkipSegmentView.show();
if (!((segment.category.behaviour.skip && !(segment.category.behaviour.key.equals("skip-once") && segment.didAutoSkipped)) || wasClicked))
return;
sendViewRequestAsync(millis, segment);
skipSegment(segment, wasClicked);
break;
}
SkipSegmentView.hide();
} catch (Exception ex) {
LogHelper.printException(() -> "findAndSkipSegment failure", ex);
}
}
private static void skipSegment(SponsorSegment segment, boolean wasClicked) {
try {
// if (lastSkippedSegment == segment) return;
// lastSkippedSegment = segment;
LogHelper.printDebug(() -> "Skipping segment: " + segment.toString());
if (SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean() && !wasClicked)
SkipSegmentView.notifySkipped(segment);
boolean didSucceed = skipToMillisecond(segment.end + 2);
if (didSucceed && !wasClicked) {
segment.didAutoSkipped = true;
}
SkipSegmentView.hide();
if (segment.category == SponsorBlockSettings.SegmentInfo.UNSUBMITTED) {
SponsorSegment[] newSegments = new SponsorSegment[sponsorSegmentsOfCurrentVideo.length - 1];
int i = 0;
for (SponsorSegment sponsorSegment : sponsorSegmentsOfCurrentVideo) {
if (sponsorSegment != segment)
newSegments[i++] = sponsorSegment;
}
sponsorSegmentsOfCurrentVideo = newSegments;
}
} catch (Exception ex) {
LogHelper.printException(() -> "skipSegment failure", ex);
}
}
}

View File

@ -0,0 +1,595 @@
package app.revanced.integrations.sponsorblock;
import static app.revanced.integrations.utils.StringRef.str;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.patches.playback.speed.RememberPlaybackSpeedPatch;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.sponsorblock.requests.SBRequester;
import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/**
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
*
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
*/
public class SegmentPlaybackController {
@Nullable
private static String currentVideoId;
@Nullable
private static SponsorSegment[] segmentsOfCurrentVideo;
/**
* Current segment that user can manually skip
*/
@Nullable
private static SponsorSegment segmentCurrentlyPlaying;
/**
* Currently playing manual skip segment, that is scheduled to hide.
* This will always be NULL or equal to {@link #segmentCurrentlyPlaying}
*/
@Nullable
private static SponsorSegment scheduledHideSegment;
/**
* Upcoming segment that is scheduled to either autoskip or show the manual skip button
*/
@Nullable
private static SponsorSegment scheduledUpcomingSegment;
@Nullable
private static String timeWithoutSegments;
private static float sponsorBarLeft = 1f;
private static float sponsorBarRight = 1f;
private static float sponsorBarThickness = 2f;
@Nullable
public static SponsorSegment[] getSegmentsOfCurrentVideo() {
return segmentsOfCurrentVideo;
}
static void setSegmentsOfCurrentVideo(@NonNull SponsorSegment[] segments) {
Arrays.sort(segments);
segmentsOfCurrentVideo = segments;
calculateTimeWithoutSegments();
}
public static boolean currentVideoHasSegments() {
return segmentsOfCurrentVideo != null && segmentsOfCurrentVideo.length > 0;
}
@Nullable
static String getCurrentVideoId() {
return currentVideoId;
}
/**
* Clears all downloaded data
*/
private static void clearData() {
currentVideoId = null;
segmentsOfCurrentVideo = null;
timeWithoutSegments = null;
segmentCurrentlyPlaying = null;
scheduledUpcomingSegment = null; // prevent any existing scheduled skip from running
scheduledHideSegment = null;
toastSegmentSkipped = null; // prevent any scheduled skip toasts from showing
toastNumberOfSegmentsSkipped = 0;
}
/**
* Injection point.
* Initializes SponsorBlock when the video player starts playing a new video.
*/
public static void initialize(Object _o) {
try {
ReVancedUtils.verifyOnMainThread();
SponsorBlockSettings.initialize();
clearData();
SponsorBlockViewController.hideSkipButton();
SponsorBlockViewController.hideNewSegmentLayout();
SponsorBlockUtils.clearUnsubmittedSegmentTimes();
LogHelper.printDebug(() -> "Initialized SponsorBlock");
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to initialize SponsorBlock", ex);
}
}
/**
* Injection point.
*/
public static void setCurrentVideoId(@Nullable String videoId) {
try {
if (Objects.equals(currentVideoId, videoId)) {
return;
}
clearData();
if (videoId == null || !SettingsEnum.SB_ENABLED.getBoolean()) {
return;
}
if (PlayerType.getCurrent().isNoneOrHidden()) {
LogHelper.printDebug(() -> "ignoring short or story");
return;
}
if (!ReVancedUtils.isNetworkConnected()) {
LogHelper.printDebug(() -> "Network not connected, ignoring video");
return;
}
currentVideoId = videoId;
LogHelper.printDebug(() -> "setCurrentVideoId: " + videoId);
//noinspection UnnecessaryLocalVariable
String videoIdToDownload = videoId; // make a copy, to use off main thread
ReVancedUtils.runOnBackgroundThread(() -> {
try {
executeDownloadSegments(videoIdToDownload);
} catch (Exception e) {
LogHelper.printException(() -> "Failed to download segments", e);
}
});
} catch (Exception ex) {
LogHelper.printException(() -> "setCurrentVideoId failure", ex);
}
}
/**
* Must be called off main thread
*/
static void executeDownloadSegments(@NonNull String videoId) {
Objects.requireNonNull(videoId);
try {
SponsorSegment[] segments = SBRequester.getSegments(videoId);
ReVancedUtils.runOnMainThread(()-> {
if (!videoId.equals(currentVideoId)) {
// user changed videos before get segments network call could complete
LogHelper.printDebug(() -> "Ignoring segments for prior video: " + videoId);
return;
}
setSegmentsOfCurrentVideo(segments);
setVideoTime(VideoInformation.getVideoTime()); // check for any skips now, instead of waiting for the next update
});
} catch (Exception ex) {
LogHelper.printException(() -> "executeDownloadSegments failure", ex);
}
}
/**
* Injection point.
* Updates SponsorBlock every 1000ms.
* When changing videos, this is first called with value 0 and then the video is changed.
*/
public static void setVideoTime(long millis) {
try {
if (!SettingsEnum.SB_ENABLED.getBoolean()
|| PlayerType.getCurrent().isNoneOrHidden() // shorts playback
|| segmentsOfCurrentVideo == null || segmentsOfCurrentVideo.length == 0) {
return;
}
LogHelper.printDebug(() -> "setVideoTime: " + millis);
// to debug the timing logic, set this to a very large value (5000 or more)
// then try manually seeking just playback reaches a skip/hide of different segments
final long lookAheadMilliseconds = 1500; // must be larger than the average time between calls to this method
final float playbackSpeed = RememberPlaybackSpeedPatch.getCurrentPlaybackSpeed();
final long startTimerLookAheadThreshold = millis + (long)(playbackSpeed * lookAheadMilliseconds);
SponsorSegment foundCurrentSegment = null;
SponsorSegment foundUpcomingSegment = null;
for (final SponsorSegment segment : segmentsOfCurrentVideo) {
if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR
|| segment.category.behaviour == CategoryBehaviour.IGNORE) {
continue;
}
if (segment.end <= millis) {
continue; // past this segment
}
if (segment.start <= millis) {
// we are in the segment!
if (segment.shouldAutoSkip()) {
skipSegment(segment, false);
return; // must return, as skipping causes a recursive call back into this method
}
// first found segment, or it's an embedded segment and fully inside the outer segment
if (foundCurrentSegment == null || foundCurrentSegment.containsSegment(segment)) {
// If the found segment is not currently displayed, then do not show if the segment is nearly over.
// This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time.
// Also prevents showing the skip button if user seeks into the last half second of the segment.
final long minMillisOfSegmentRemainingThreshold = 500;
if (segmentCurrentlyPlaying == segment
|| !segment.timeIsNearEnd(millis, minMillisOfSegmentRemainingThreshold)) {
foundCurrentSegment = segment;
} else {
LogHelper.printDebug(() -> "Ignoring segment that ends very soon: " + segment);
}
}
// Keep iterating and looking. There may be an upcoming autoskip,
// or there may be another smaller segment nested inside this segment
continue;
}
// segment is upcoming
if (startTimerLookAheadThreshold < segment.start) {
break; // segment is not close enough to schedule, and no segments after this are of interest
}
if (segment.shouldAutoSkip()) { // upcoming autoskip
foundUpcomingSegment = segment;
break; // must stop here
}
// upcoming manual skip
// do not schedule upcoming segment, if it is not fully contained inside the current segment
if ((foundCurrentSegment == null || foundCurrentSegment.containsSegment(segment))
// use the most inner upcoming segment
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
// Only schedule, if the segment start time is not near the end time of the current segment.
// This check is needed to prevent scheduled hide and show from clashing with each other.
final long minTimeBetweenStartEndOfSegments = 1000;
if (foundCurrentSegment == null
|| !foundCurrentSegment.timeIsNearEnd(segment.start, minTimeBetweenStartEndOfSegments)) {
foundUpcomingSegment = segment;
} else {
LogHelper.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment);
}
}
}
if (segmentCurrentlyPlaying != foundCurrentSegment) {
if (foundCurrentSegment == null) {
LogHelper.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying);
segmentCurrentlyPlaying = null;
SponsorBlockViewController.hideSkipButton();
} else {
segmentCurrentlyPlaying = foundCurrentSegment;
LogHelper.printDebug(() -> "Showing segment: " + segmentCurrentlyPlaying);
SponsorBlockViewController.showSkipButton(foundCurrentSegment);
}
}
// must be greater than the average time between updates to VideoInformation time
final long videoInformationTimeUpdateThresholdMilliseconds = 250;
// schedule a hide, only if the segment end is near
final SponsorSegment segmentToHide =
(foundCurrentSegment != null && foundCurrentSegment.timeIsNearEnd(millis, lookAheadMilliseconds))
? foundCurrentSegment
: null;
if (scheduledHideSegment != segmentToHide) {
if (segmentToHide == null) {
LogHelper.printDebug(() -> "Clearing scheduled hide: " + scheduledHideSegment);
scheduledHideSegment = null;
} else {
scheduledHideSegment = segmentToHide;
LogHelper.printDebug(() -> "Scheduling hide segment: " + segmentToHide + " playbackSpeed: " + playbackSpeed);
final long delayUntilHide = (long) ((segmentToHide.end - millis) / playbackSpeed);
ReVancedUtils.runOnMainThreadDelayed(() -> {
if (scheduledHideSegment != segmentToHide) {
LogHelper.printDebug(() -> "Ignoring old scheduled hide segment: " + segmentToHide);
return;
}
scheduledHideSegment = null;
final long videoTime = VideoInformation.getVideoTime();
if (!segmentToHide.timeIsNearEnd(videoTime, videoInformationTimeUpdateThresholdMilliseconds)) {
// current video time is not what's expected. User paused playback
LogHelper.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
+ " videoInformation time: " + videoTime);
return;
}
LogHelper.printDebug(() -> "Running scheduled hide segment: " + segmentToHide);
// Need more than just hide the skip button, as this may have been an embedded segment
// Instead call back into setVideoTime to check everything again.
// Should not use VideoInformation time as it is less accurate,
// but this scheduled handler was scheduled precisely so we can just use the segment end time
segmentCurrentlyPlaying = null;
SponsorBlockViewController.hideSkipButton();
setVideoTime(segmentToHide.end);
}, delayUntilHide);
}
}
if (scheduledUpcomingSegment != foundUpcomingSegment) {
if (foundUpcomingSegment == null) {
LogHelper.printDebug(() -> "Clearing scheduled segment: " + scheduledUpcomingSegment);
scheduledUpcomingSegment = null;
} else {
scheduledUpcomingSegment = foundUpcomingSegment;
final SponsorSegment segmentToSkip = foundUpcomingSegment;
LogHelper.printDebug(() -> "Scheduling segment: " + segmentToSkip + " playbackSpeed: " + playbackSpeed);
final long delayUntilSkip = (long) ((segmentToSkip.start - millis) / playbackSpeed);
ReVancedUtils.runOnMainThreadDelayed(() -> {
if (scheduledUpcomingSegment != segmentToSkip) {
LogHelper.printDebug(() -> "Ignoring old scheduled segment: " + segmentToSkip);
return;
}
scheduledUpcomingSegment = null;
final long videoTime = VideoInformation.getVideoTime();
if (!segmentToSkip.timeIsNearStart(videoTime,
videoInformationTimeUpdateThresholdMilliseconds)) {
// current video time is not what's expected. User paused playback
LogHelper.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
+ " videoInformation time: " + videoTime);
return;
}
if (segmentToSkip.shouldAutoSkip()) {
LogHelper.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip);
skipSegment(segmentToSkip, false);
} else {
LogHelper.printDebug(() -> "Running scheduled show segment: " + segmentToSkip);
segmentCurrentlyPlaying = segmentToSkip;
SponsorBlockViewController.showSkipButton(segmentToSkip);
}
}, delayUntilSkip);
}
}
} catch (Exception e) {
LogHelper.printException(() -> "setVideoTime failure", e);
}
}
private static SponsorSegment lastSegmentSkipped;
private static long lastSegmentSkippedTime;
private static void skipSegment(@NonNull SponsorSegment segment, boolean userManuallySkipped) {
try {
// If trying to seek to end of the video, YouTube can seek just short of the actual end.
// (especially if the video does not end on a whole second boundary).
// This causes additional segment skip attempts, even though it cannot seek any closer to the desired time.
// Check for and ignore repeated skip attempts of the same segment over a short time period.
final long now = System.currentTimeMillis();
final long minimumMillisecondsBetweenSkippingSameSegment = 500;
if ((lastSegmentSkipped == segment) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) {
LogHelper.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segment);
return;
}
LogHelper.printDebug(() -> "Skipping segment: " + segment);
lastSegmentSkipped = segment;
lastSegmentSkippedTime = now;
segmentCurrentlyPlaying = null;
scheduledHideSegment = null; // if a scheduled has not run yet
scheduledUpcomingSegment = null;
SponsorBlockViewController.hideSkipButton();
final boolean seekSuccessful = VideoInformation.seekTo(segment.end);
if (!seekSuccessful) {
// can happen when switching videos and is normal
LogHelper.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segment);
return;
}
if (!userManuallySkipped) {
// check for any smaller embedded segments, and count those as autoskipped
final boolean showSkipToast = SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean();
for (final SponsorSegment otherSegment : segmentsOfCurrentVideo) {
if (segment.end <= otherSegment.start) {
break; // no other segments can be contained
}
if (segment.containsSegment(otherSegment)) { // includes checking the segment against itself
otherSegment.didAutoSkipped = true; // skipped this segment as well
if (showSkipToast) {
showSkippedSegmentToast(otherSegment);
}
}
}
}
if (segment.category == SegmentCategory.UNSUBMITTED) {
// skipped segment was a preview of unsubmitted segment
// remove the segment from the UI view
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
SponsorSegment[] newSegments = new SponsorSegment[segmentsOfCurrentVideo.length - 1];
int i = 0;
for (SponsorSegment sponsorSegment : segmentsOfCurrentVideo) {
if (sponsorSegment != segment)
newSegments[i++] = sponsorSegment;
}
setSegmentsOfCurrentVideo(newSegments);
} else {
SponsorBlockUtils.sendViewRequestAsync(segment);
}
} catch (Exception ex) {
LogHelper.printException(() -> "skipSegment failure", ex);
}
}
private static int toastNumberOfSegmentsSkipped;
@Nullable
private static SponsorSegment toastSegmentSkipped;
private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) {
ReVancedUtils.verifyOnMainThread();
toastNumberOfSegmentsSkipped++;
if (toastNumberOfSegmentsSkipped > 1) {
return; // toast already scheduled
}
toastSegmentSkipped = segment;
final long delayToToastMilliseconds = 200; // also the maximum time between skips to be considered skipping multiple segments
ReVancedUtils.runOnMainThreadDelayed(() -> {
try {
if (toastSegmentSkipped == null) { // video was changed just after skipping segment
LogHelper.printDebug(() -> "Ignoring old scheduled show toast");
return;
}
ReVancedUtils.showToastShort(toastNumberOfSegmentsSkipped == 1
? toastSegmentSkipped.getSkippedToastText()
: str("sb_skipped_multiple_segments"));
} catch (Exception ex) {
LogHelper.printException(() -> "showSkippedSegmentToast failure", ex);
} finally {
toastNumberOfSegmentsSkipped = 0;
toastSegmentSkipped = null;
}
}, delayToToastMilliseconds);
}
public static void onSkipSponsorClicked() {
if (segmentCurrentlyPlaying != null) {
skipSegment(segmentCurrentlyPlaying, true);
} else {
SponsorBlockViewController.hideSkipButton();
LogHelper.printException(() -> "error: segment not available to skip"); // should never happen
}
}
/**
* Injection point
*/
public static void setSponsorBarAbsoluteLeft(final Rect rect) {
setSponsorBarAbsoluteLeft(rect.left);
}
public static void setSponsorBarAbsoluteLeft(final float left) {
if (sponsorBarLeft != left) {
LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteLeft: left=%.2f", left));
sponsorBarLeft = left;
}
}
/**
* Injection point
*/
public static void setSponsorBarRect(final Object self) {
try {
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
field.setAccessible(true);
Rect rect = (Rect) field.get(self);
if (rect == null) {
LogHelper.printException(() -> "Could not find sponsorblock rect");
} else {
setSponsorBarAbsoluteLeft(rect.left);
setSponsorBarAbsoluteRight(rect.right);
}
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarRect failure", ex);
}
}
/**
* Injection point
*/
public static void setSponsorBarAbsoluteRight(final Rect rect) {
setSponsorBarAbsoluteRight(rect.right);
}
public static void setSponsorBarAbsoluteRight(final float right) {
if (sponsorBarRight != right) {
LogHelper.printDebug(() -> String.format("setSponsorBarAbsoluteRight: right=%.2f", right));
sponsorBarRight = right;
}
}
/**
* Injection point
*/
public static void setSponsorBarThickness(final int thickness) {
try {
setSponsorBarThickness((float) thickness);
} catch (Exception ex) {
LogHelper.printException(() -> "setSponsorBarThickness failure", ex);
}
}
public static void setSponsorBarThickness(final float thickness) {
if (sponsorBarThickness != thickness) {
LogHelper.printDebug(() -> String.format("setSponsorBarThickness: %.2f", thickness));
sponsorBarThickness = thickness;
}
}
/**
* Injection point
*/
public static String appendTimeWithoutSegments(String totalTime) {
try {
if (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()
&& !TextUtils.isEmpty(totalTime) && !TextUtils.isEmpty(timeWithoutSegments)) {
return totalTime + timeWithoutSegments;
}
} catch (Exception ex) {
LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex);
}
return totalTime;
}
private static void calculateTimeWithoutSegments() {
final long currentVideoLength = VideoInformation.getCurrentVideoLength();
if (!SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean() || currentVideoLength <= 0
|| segmentsOfCurrentVideo == null || segmentsOfCurrentVideo.length == 0) {
timeWithoutSegments = null;
return;
}
long timeWithoutSegmentsValue = currentVideoLength + 500; // YouTube:tm:
for (SponsorSegment segment : segmentsOfCurrentVideo) {
timeWithoutSegmentsValue -= segment.length();
}
final long hours = timeWithoutSegmentsValue / 3600000;
final long minutes = (timeWithoutSegmentsValue / 60000) % 60;
final long seconds = (timeWithoutSegmentsValue / 1000) % 60;
if (hours > 0) {
timeWithoutSegments = String.format("\u2009(%d:%02d:%02d)", hours, minutes, seconds);
} else {
timeWithoutSegments = String.format("\u2009(%d:%02d)", minutes, seconds);
}
}
/**
* Injection point
*/
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
try {
if (sponsorBarThickness < 0.1) return;
if (segmentsOfCurrentVideo == null) return;
final long currentVideoLength = VideoInformation.getCurrentVideoLength();
if (currentVideoLength <= 0) return;
final float thicknessDiv2 = sponsorBarThickness / 2;
final float top = posY - thicknessDiv2;
final float bottom = posY + thicknessDiv2;
final float absoluteLeft = sponsorBarLeft;
final float absoluteRight = sponsorBarRight;
final float tmp1 = (1f / currentVideoLength) * (absoluteRight - absoluteLeft);
for (SponsorSegment segment : segmentsOfCurrentVideo) {
float left = segment.start * tmp1 + absoluteLeft;
float right = segment.end * tmp1 + absoluteLeft;
canvas.drawRect(left, top, right, bottom, segment.category.paint);
}
} catch (Exception ex) {
LogHelper.printException(() -> "drawSponsorTimeBars failure", ex);
}
}
}

View File

@ -1,120 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.content.Context;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength;
import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class ShieldButton {
static RelativeLayout _youtubeControlsLayout;
static WeakReference<ImageView> _shieldBtn = new WeakReference<>(null);
static int fadeDurationFast;
static int fadeDurationScheduled;
static Animation fadeIn;
static Animation fadeOut;
static boolean isShowing;
public static void initialize(Object viewStub) {
try {
LogHelper.printDebug(() -> "initializing shield button");
_youtubeControlsLayout = (RelativeLayout) viewStub;
ImageView imageView = (ImageView) _youtubeControlsLayout
.findViewById(getIdentifier("sponsorblock_button", "id"));
if (imageView == null) {
LogHelper.printDebug(() -> "Couldn't find imageView with \"sponsorblock_button\"");
}
if (imageView == null) return;
imageView.setOnClickListener(SponsorBlockUtils.sponsorBlockBtnListener);
_shieldBtn = new WeakReference<>(imageView);
// Animations
fadeDurationFast = getInteger("fade_duration_fast");
fadeDurationScheduled = getInteger("fade_duration_scheduled");
fadeIn = getAnimation("fade_in");
fadeIn.setDuration(fadeDurationFast);
fadeOut = getAnimation("fade_out");
fadeOut.setDuration(fadeDurationScheduled);
isShowing = true;
changeVisibilityImmediate(false);
} catch (Exception ex) {
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
}
}
public static void changeVisibilityImmediate(boolean visible) {
changeVisibility(visible, true);
}
public static void changeVisibilityNegatedImmediate(boolean visible) {
changeVisibility(!visible, true);
}
public static void changeVisibility(boolean visible) {
changeVisibility(visible, false);
}
public static void changeVisibility(boolean visible, boolean immediate) {
try {
if (isShowing == visible) return;
isShowing = visible;
ImageView iView = _shieldBtn.get();
if (_youtubeControlsLayout == null || iView == null) return;
if (visible && shouldBeShown()) {
if (getLastKnownVideoTime() >= getCurrentVideoLength()) {
return;
}
LogHelper.printDebug(() -> "Fading in");
iView.setVisibility(View.VISIBLE);
if (!immediate)
iView.startAnimation(fadeIn);
return;
}
if (iView.getVisibility() == View.VISIBLE) {
LogHelper.printDebug(() -> "Fading out");
if (!immediate)
iView.startAnimation(fadeOut);
iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE);
}
} catch (Exception ex) {
LogHelper.printException(() -> "changeVisibility failure", ex);
}
}
static boolean shouldBeShown() {
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean();
}
//region Helpers
private static int getIdentifier(String name, String defType) {
Context context = ReVancedUtils.getContext();
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
private static int getInteger(String name) {
return ReVancedUtils.getContext().getResources().getInteger(getIdentifier(name, "integer"));
}
private static Animation getAnimation(String name) {
return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), getIdentifier(name, "anim"));
}
//endregion
}

View File

@ -1,46 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.DisplayMetrics;
import android.widget.Toast;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.utils.ReVancedUtils;
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.hideSkipButton;
import static app.revanced.integrations.sponsorblock.player.ui.SponsorBlockView.showSkipButton;
@SuppressLint({"RtlHardcoded", "SetTextI18n", "AppCompatCustomView"})
public class SkipSegmentView {
private static SponsorSegment lastNotifiedSegment;
public static void show() {
showSkipButton();
}
public static void hide() {
hideSkipButton();
}
public static void notifySkipped(SponsorSegment segment) {
if (segment == lastNotifiedSegment) {
LogHelper.printDebug(() -> "notifySkipped; segment == lastNotifiedSegment");
return;
}
lastNotifiedSegment = segment;
String skipMessage = segment.category.skipMessage.toString();
Context context = ReVancedUtils.getContext();
LogHelper.printDebug(() -> String.format("notifySkipped; message=%s", skipMessage));
if (context != null)
Toast.makeText(context, skipMessage, Toast.LENGTH_SHORT).show();
}
public static float convertDpToPixel(float dp, Context context) {
return dp * ((float) context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
}

View File

@ -1,202 +1,195 @@
package app.revanced.integrations.sponsorblock;
import static app.revanced.integrations.sponsorblock.StringRef.sf;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.Activity;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Html;
import android.text.TextUtils;
import android.util.Patterns;
import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
public class SponsorBlockSettings {
public static final String CATEGORY_COLOR_SUFFIX = "_color";
public static final SegmentBehaviour DefaultBehaviour = SegmentBehaviour.IGNORE;
public static String sponsorBlockUrlCategories = "[]";
public static void importSettings(@NonNull String json) {
ReVancedUtils.verifyOnMainThread();
try {
JSONObject settingsJson = new JSONObject(json);
JSONObject barTypesObject = settingsJson.getJSONObject("barTypes");
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
public static void update(Activity _activity) {
SharedPreferences preferences = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK);
if (!SettingsEnum.SB_ENABLED.getBoolean()) {
SkipSegmentView.hide();
NewSegmentHelperLayout.hide();
SponsorBlockUtils.hideShieldButton();
SponsorBlockUtils.hideVoteButton();
PlayerController.sponsorSegmentsOfCurrentVideo = null;
} else { /*isAddNewSegmentEnabled*/
SponsorBlockUtils.showShieldButton();
for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) {
// clear existing behavior, as browser plugin exports no value for ignored categories
category.behaviour = CategoryBehaviour.IGNORE;
JSONObject categoryObject = barTypesObject.getJSONObject(category.key);
category.setColor(categoryObject.getString("color"));
}
if (!SettingsEnum.SB_NEW_SEGMENT_ENABLED.getBoolean()) {
NewSegmentHelperLayout.hide();
SponsorBlockUtils.hideShieldButton();
} else {
SponsorBlockUtils.showShieldButton();
for (int i = 0; i < categorySelectionsArray.length(); i++) {
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
String categoryKey = categorySelectionObject.getString("name");
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
if (category == null) {
continue; // unsupported category, ignore
}
if (!SettingsEnum.SB_VOTING_ENABLED.getBoolean())
SponsorBlockUtils.hideVoteButton();
else
SponsorBlockUtils.showVoteButton();
SegmentBehaviour[] possibleBehaviours = SegmentBehaviour.values();
final ArrayList<String> enabledCategories = new ArrayList<>(possibleBehaviours.length);
for (SegmentInfo segment : SegmentInfo.values()) {
String categoryColor = preferences.getString(segment.key + CATEGORY_COLOR_SUFFIX, SponsorBlockUtils.formatColorString(segment.defaultColor));
segment.setColor(Color.parseColor(categoryColor));
SegmentBehaviour behaviour = null;
String value = preferences.getString(segment.key, null);
if (value != null) {
for (SegmentBehaviour possibleBehaviour : possibleBehaviours) {
if (possibleBehaviour.key.equals(value)) {
behaviour = possibleBehaviour;
break;
}
}
}
final int desktopKey = categorySelectionObject.getInt("option");
CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKey(desktopKey);
if (behaviour != null) {
segment.behaviour = behaviour;
category.behaviour = behaviour;
} else {
behaviour = segment.behaviour;
LogHelper.printException(() -> "Unknown segment category behavior key: " + desktopKey);
}
}
SegmentCategory.updateEnabledCategories();
SharedPreferences.Editor editor = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK).edit();
for (SegmentCategory category : SegmentCategory.valuesWithoutUnsubmitted()) {
category.save(editor);
}
editor.apply();
String userID = settingsJson.getString("userID");
if (!isValidSBUserId(userID)) {
throw new IllegalArgumentException("userId is blank");
}
SettingsEnum.SB_UUID.saveValue(userID);
SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip"));
SettingsEnum.SB_SHOW_TOAST_ON_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice"));
SettingsEnum.SB_TRACK_SKIP_COUNT.saveValue(settingsJson.getBoolean("trackViewCount"));
String serverAddress = settingsJson.getString("serverAddress");
if (!isValidSBServerAddress(serverAddress)) {
throw new IllegalArgumentException(str("sb_api_url_invalid"));
}
SettingsEnum.SB_API_URL.saveValue(serverAddress);
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips"));
final float minDuration = (float)settingsJson.getDouble("minDuration");
if (minDuration < 0) {
throw new IllegalArgumentException("invalid minDuration: " + minDuration);
}
SettingsEnum.SB_MIN_DURATION.saveValue(minDuration);
try {
int skipCount = settingsJson.getInt("skipCount");
if (skipCount < 0) {
throw new IllegalArgumentException("invalid skipCount: " + skipCount);
}
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(skipCount);
final double minutesSaved = settingsJson.getDouble("minutesSaved");
if (minutesSaved < 0) {
throw new IllegalArgumentException("invalid minutesSaved: " + minutesSaved);
}
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue((long)(minutesSaved * 60 * 1000));
} catch (JSONException ex) {
// ignore. values were not exported in prior versions of ReVanced
}
if (behaviour.showOnTimeBar && segment != SegmentInfo.UNSUBMITTED)
enabledCategories.add(segment.key);
ReVancedUtils.showToastLong(str("sb_settings_import_successful"));
} catch (Exception ex) {
LogHelper.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast
ReVancedUtils.showToastLong(str("sb_settings_import_failed", ex.getMessage()));
}
}
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]";
if (enabledCategories.isEmpty())
sponsorBlockUrlCategories = "[]";
else
sponsorBlockUrlCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
@NonNull
public static String exportSettings() {
ReVancedUtils.verifyOnMainThread();
try {
LogHelper.printDebug(() -> "Creating SponsorBlock export settings string");
JSONObject json = new JSONObject();
JSONObject barTypesObject = new JSONObject(); // categories' colors
JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted();
for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject();
String categoryKey = category.key;
categoryObject.put("color", category.colorString());
barTypesObject.put(categoryKey, categoryObject);
JSONObject behaviorObject = new JSONObject();
behaviorObject.put("name", categoryKey);
behaviorObject.put("option", category.behaviour.desktopKey);
categorySelectionsArray.put(behaviorObject);
}
json.put("userID", SettingsEnum.SB_UUID.getString());
json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean());
json.put("serverAddress", SettingsEnum.SB_API_URL.getString());
json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_ON_SKIP.getBoolean());
json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat());
json.put("trackViewCount", SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean());
json.put("skipCount", SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt());
json.put("minutesSaved", SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() / (60f * 1000));
json.put("categorySelections", categorySelectionsArray);
json.put("barTypes", barTypesObject);
return json.toString(2);
} catch (Exception ex) {
LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast
ReVancedUtils.showToastLong(str("sb_settings_export_failed"));
return "";
}
}
public static boolean isValidSBUserId(@NonNull String userId) {
return !userId.isEmpty();
}
/**
* A non comprehensive check if a SB api server address is valid.
*/
public static boolean isValidSBServerAddress(@NonNull String serverAddress) {
if (!Patterns.WEB_URL.matcher(serverAddress).matches()) {
return false;
}
// Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/"
// Could use Patterns.compile, but this is simpler
final int lastDotIndex = serverAddress.lastIndexOf('.');
if (lastDotIndex != -1 && serverAddress.substring(lastDotIndex).contains("/")) {
return false;
}
// Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)"
// but that should not be done on the main thread.
// Instead, assume the domain exists and the user knows what they're doing.
return true;
}
private static boolean initialized;
public static void initialize() {
if (initialized) {
return;
}
initialized = true;
String uuid = SettingsEnum.SB_UUID.getString();
if (uuid == null || uuid.length() == 0) {
if (uuid == null || uuid.isEmpty()) {
uuid = (UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString())
.replace("-", "");
SettingsEnum.SB_UUID.saveValue(uuid);
}
}
public enum SegmentBehaviour {
SKIP_AUTOMATICALLY_ONCE("skip-once", 3, sf("skip_automatically_once"), true, true),
SKIP_AUTOMATICALLY("skip", 2, sf("skip_automatically"), true, true),
MANUAL_SKIP("manual-skip", 1, sf("skip_showbutton"), false, true),
IGNORE("ignore", -1, sf("skip_ignore"), false, false);
public final String key;
public final int desktopKey;
public final StringRef name;
public final boolean skip;
public final boolean showOnTimeBar;
SegmentBehaviour(String key,
int desktopKey,
StringRef name,
boolean skip,
boolean showOnTimeBar) {
this.key = key;
this.desktopKey = desktopKey;
this.name = name;
this.skip = skip;
this.showOnTimeBar = showOnTimeBar;
}
public static SegmentBehaviour byDesktopKey(int desktopKey) {
for (SegmentBehaviour behaviour : values()) {
if (behaviour.desktopKey == desktopKey) {
return behaviour;
}
}
return null;
}
}
public enum SegmentInfo {
SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFF00d400),
INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFF00ffff),
OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFF0202ed),
INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFcc00ff),
SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFffff00),
MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), SegmentBehaviour.MANUAL_SKIP, 0xFFff9900),
PREVIEW("preview", sf("segments_preview"), sf("skipped_preview"), sf("segments_preview_sum"), DefaultBehaviour, 0xFF008fd6),
FILLER("filler", sf("segments_filler"), sf("skipped_filler"), sf("segments_filler_sum"), DefaultBehaviour, 0xFF7300FF),
UNSUBMITTED("unsubmitted", StringRef.empty, sf("skipped_unsubmitted"), StringRef.empty, SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFFFFFFF);
private static final SegmentInfo[] mValuesWithoutUnsubmitted = new SegmentInfo[]{
SPONSOR,
INTRO,
OUTRO,
INTERACTION,
SELF_PROMO,
MUSIC_OFFTOPIC,
PREVIEW,
FILLER
};
private static final Map<String, SegmentInfo> mValuesMap = new HashMap<>(values().length);
static {
for (SegmentInfo value : valuesWithoutUnsubmitted())
mValuesMap.put(value.key, value);
}
public final String key;
public final StringRef title;
public final StringRef skipMessage;
public final StringRef description;
public final Paint paint;
public final int defaultColor;
public int color;
public SegmentBehaviour behaviour;
SegmentInfo(String key,
StringRef title,
StringRef skipMessage,
StringRef description,
SegmentBehaviour behaviour,
int defaultColor) {
this.key = key;
this.title = title;
this.skipMessage = skipMessage;
this.description = description;
this.behaviour = behaviour;
this.defaultColor = defaultColor;
this.color = defaultColor;
this.paint = new Paint();
}
public static SegmentInfo[] valuesWithoutUnsubmitted() {
return mValuesWithoutUnsubmitted;
}
public static SegmentInfo byCategoryKey(String key) {
return mValuesMap.get(key);
}
public void setColor(int color) {
color = color & 0xFFFFFF;
this.color = color;
paint.setColor(color);
paint.setAlpha(255);
}
public CharSequence getTitleWithDot() {
return Html.fromHtml(String.format("<font color=\"#%06X\">⬤</font> %s", color, title));
}
SegmentCategory.loadFromPreferences();
}
}

View File

@ -1,41 +1,20 @@
package app.revanced.integrations.sponsorblock;
import static android.text.Html.fromHtml;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoId;
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength;
import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime;
import static app.revanced.integrations.sponsorblock.PlayerController.sponsorSegmentsOfCurrentVideo;
import static app.revanced.integrations.settingsmenu.SponsorBlockSettingsFragment.FORMATTER;
import static app.revanced.integrations.settingsmenu.SponsorBlockSettingsFragment.SAVED_TEMPLATE;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import static app.revanced.integrations.sponsorblock.requests.SBRequester.voteForSegment;
import static app.revanced.integrations.utils.StringRef.str;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.text.Html;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONObject;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@ -43,164 +22,157 @@ import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.player.PlayerType;
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote;
import app.revanced.integrations.sponsorblock.requests.SBRequester;
import app.revanced.integrations.sponsorblock.ui.SponsorBlockViewController;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.sponsorblock.objects.UserStats;
import app.revanced.integrations.sponsorblock.requests.SBRequester;
public abstract class SponsorBlockUtils {
public static final String DATE_FORMAT = "HH:mm:ss.SSS";
/**
* Not thread safe. All fields/methods must be accessed from the main thread.
*/
public class SponsorBlockUtils {
private static final String MANUAL_EDIT_TIME_FORMAT = "HH:mm:ss.SSS";
@SuppressLint("SimpleDateFormat")
public static final SimpleDateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
public static boolean videoHasSegments = false;
public static String timeWithoutSegments = "";
private static final int sponsorBtnId = 1234;
private static final SimpleDateFormat manualEditTimeFormatter = new SimpleDateFormat(MANUAL_EDIT_TIME_FORMAT);
@SuppressLint("SimpleDateFormat")
private static final SimpleDateFormat voteSegmentTimeFormatter = new SimpleDateFormat();
static {
TimeZone utc = TimeZone.getTimeZone("UTC");
manualEditTimeFormatter.setTimeZone(utc);
voteSegmentTimeFormatter.setTimeZone(utc);
}
private static final String LOCKED_COLOR = "#FFC83D";
public static final View.OnClickListener sponsorBlockBtnListener = v -> {
LogHelper.printDebug(() -> "Shield button clicked");
NewSegmentHelperLayout.toggle();
};
public static final View.OnClickListener voteButtonListener = v -> {
LogHelper.printDebug(() -> "Vote button clicked");
SponsorBlockUtils.onVotingClicked(v.getContext());
};
private static int shareBtnId = -1;
private static long newSponsorSegmentDialogShownMillis;
private static long newSponsorSegmentStartMillis = -1;
private static long newSponsorSegmentEndMillis = -1;
private static boolean newSponsorSegmentPreviewed;
private static final DialogInterface.OnClickListener newSponsorSegmentDialogListener = new DialogInterface.OnClickListener() {
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
Context context = ((AlertDialog) dialog).getContext();
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
// start
newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis;
Toast.makeText(context.getApplicationContext(), str("new_segment_time_start_set"), Toast.LENGTH_LONG).show();
break;
case DialogInterface.BUTTON_POSITIVE:
// end
newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis;
Toast.makeText(context.getApplicationContext(), str("new_segment_time_end_set"), Toast.LENGTH_SHORT).show();
break;
}
dialog.dismiss();
}
};
private static SponsorBlockSettings.SegmentInfo newSponsorBlockSegmentType;
private static SegmentCategory newUserCreatedSegmentCategory;
private static final DialogInterface.OnClickListener segmentTypeListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted()[which];
try {
SegmentCategory category = SegmentCategory.valuesWithoutUnsubmitted()[which];
boolean enableButton;
if (!segmentType.behaviour.showOnTimeBar) {
Toast.makeText(
((AlertDialog) dialog).getContext().getApplicationContext(),
str("new_segment_disabled_category"),
Toast.LENGTH_SHORT).show();
if (category.behaviour == CategoryBehaviour.IGNORE) {
ReVancedUtils.showToastLong(str("sb_new_segment_disabled_category"));
enableButton = false;
} else {
Toast.makeText(
((AlertDialog) dialog).getContext().getApplicationContext(),
segmentType.description.toString(),
Toast.LENGTH_SHORT).show();
newSponsorBlockSegmentType = segmentType;
newUserCreatedSegmentCategory = category;
enableButton = true;
}
((AlertDialog) dialog)
.getButton(DialogInterface.BUTTON_POSITIVE)
.setEnabled(enableButton);
} catch (Exception ex) {
LogHelper.printException(() -> "segmentTypeListener failure", ex);
}
}
};
private static final DialogInterface.OnClickListener segmentReadyDialogButtonListener = new DialogInterface.OnClickListener() {
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
NewSegmentHelperLayout.hide();
try {
SponsorBlockViewController.hideNewSegmentLayout();
Context context = ((AlertDialog) dialog).getContext();
dialog.dismiss();
SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
CharSequence[] titles = new CharSequence[values.length];
for (int i = 0; i < values.length; i++) {
// titles[i] = values[i].title;
titles[i] = values[i].getTitleWithDot();
SegmentCategory[] categories = SegmentCategory.valuesWithoutUnsubmitted();
CharSequence[] titles = new CharSequence[categories.length];
for (int i = 0, length = categories.length; i < length; i++) {
titles[i] = categories[i].getTitleWithColorDot();
}
newSponsorBlockSegmentType = null;
newUserCreatedSegmentCategory = null;
new AlertDialog.Builder(context)
.setTitle(str("new_segment_choose_category"))
.setTitle(str("sb_new_segment_choose_category"))
.setSingleChoiceItems(titles, -1, segmentTypeListener)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener)
.show()
.getButton(DialogInterface.BUTTON_POSITIVE)
.setEnabled(false);
} catch (Exception ex) {
LogHelper.printException(() -> "segmentReadyDialogButtonListener failure", ex);
}
}
};
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) {
private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = (dialog, which) -> {
dialog.dismiss();
Context context = ((AlertDialog) dialog).getContext().getApplicationContext();
Toast.makeText(context, str("submit_started"), Toast.LENGTH_SHORT).show();
appContext = new WeakReference<>(context);
ReVancedUtils.runOnBackgroundThread(submitRunnable);
}
submitNewSegment();
};
public static String messageToToast = "";
private static final EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener();
private static final DialogInterface.OnClickListener editByHandDialogListener = (dialog, which) -> {
try {
Context context = ((AlertDialog) dialog).getContext();
final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which;
final EditText textView = new EditText(context);
textView.setHint(DATE_FORMAT);
textView.setHint(MANUAL_EDIT_TIME_FORMAT);
if (isStart) {
if (newSponsorSegmentStartMillis >= 0)
textView.setText(dateFormatter.format(new Date(newSponsorSegmentStartMillis)));
textView.setText(manualEditTimeFormatter.format(new Date(newSponsorSegmentStartMillis)));
} else {
if (newSponsorSegmentEndMillis >= 0)
textView.setText(dateFormatter.format(new Date(newSponsorSegmentEndMillis)));
textView.setText(manualEditTimeFormatter.format(new Date(newSponsorSegmentEndMillis)));
}
editByHandSaveDialogListener.settingStart = isStart;
editByHandSaveDialogListener.editText = new WeakReference<>(textView);
new AlertDialog.Builder(context)
.setTitle(str(isStart ? "new_segment_time_start" : "new_segment_time_end"))
.setTitle(str(isStart ? "sb_new_segment_time_start" : "sb_new_segment_time_end"))
.setView(textView)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("new_segment_now"), editByHandSaveDialogListener)
.setNeutralButton(str("sb_new_segment_now"), editByHandSaveDialogListener)
.setPositiveButton(android.R.string.ok, editByHandSaveDialogListener)
.show();
dialog.dismiss();
};
private static final Runnable toastRunnable = () -> {
Context context = appContext.get();
if (context != null && messageToToast != null)
Toast.makeText(context, messageToToast, Toast.LENGTH_LONG).show();
} catch (Exception ex) {
LogHelper.printException(() -> "editByHandDialogListener failure", ex);
}
};
private static final DialogInterface.OnClickListener segmentVoteClickListener = (dialog, which) -> {
try {
final Context context = ((AlertDialog) dialog).getContext();
final SponsorSegment segment = sponsorSegmentsOfCurrentVideo[which];
SponsorSegment[] currentSegments = SegmentPlaybackController.getSegmentsOfCurrentVideo();
if (currentSegments == null || currentSegments.length == 0) {
// should never be reached
LogHelper.printException(() -> "Segment is no longer available on the client");
return;
}
SponsorSegment segment = currentSegments[which];
final VoteOption[] voteOptions = VoteOption.values();
SegmentVote[] voteOptions = SegmentVote.values();
CharSequence[] items = new CharSequence[voteOptions.length];
for (int i = 0; i < voteOptions.length; i++) {
VoteOption voteOption = voteOptions[i];
String title = voteOption.title;
SegmentVote voteOption = voteOptions[i];
String title = voteOption.title.toString();
if (SettingsEnum.SB_IS_VIP.getBoolean() && segment.isLocked && voteOption.shouldHighlight) {
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title));
} else {
@ -210,12 +182,11 @@ public abstract class SponsorBlockUtils {
new AlertDialog.Builder(context)
.setItems(items, (dialog1, which1) -> {
appContext = new WeakReference<>(context.getApplicationContext());
VoteOption voteOption = voteOptions[which1];
SegmentVote voteOption = voteOptions[which1];
switch (voteOption) {
case UPVOTE:
case DOWNVOTE:
voteForSegment(segment, voteOption, appContext.get());
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
break;
case CATEGORY_CHANGE:
onNewCategorySelect(segment, context);
@ -223,91 +194,79 @@ public abstract class SponsorBlockUtils {
}
})
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "onPreviewClicked failure", ex);
}
};
private static final Runnable submitRunnable = () -> {
messageToToast = null;
final String uuid = SettingsEnum.SB_UUID.getString();
final long start = newSponsorSegmentStartMillis;
final long end = newSponsorSegmentEndMillis;
final String videoId = getCurrentVideoId();
final SponsorBlockSettings.SegmentInfo segmentType = SponsorBlockUtils.newSponsorBlockSegmentType;
try {
if (start < 0 || end < 0 || start >= end || segmentType == null || videoId == null || uuid == null) {
LogHelper.printException(() -> "Unable to submit times, invalid parameters");
return;
}
SBRequester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable);
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
} catch (Exception e) {
LogHelper.printException(() -> "Unable to submit segment", e);
}
if (videoId != null)
PlayerController.executeDownloadSegments(videoId);
};
static {
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private SponsorBlockUtils() {
}
public static void showShieldButton() {
View i = ShieldButton._shieldBtn.get();
if (i == null || !ShieldButton.shouldBeShown()) return;
i.setVisibility(VISIBLE);
i.bringToFront();
i.requestLayout();
i.invalidate();
static void setNewSponsorSegmentPreviewed() {
newSponsorSegmentPreviewed = true;
}
public static void hideShieldButton() {
View i = ShieldButton._shieldBtn.get();
if (i != null)
i.setVisibility(GONE);
static void clearUnsubmittedSegmentTimes() {
newSponsorSegmentDialogShownMillis = 0;
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
newSponsorSegmentPreviewed = false;
}
public static void showVoteButton() {
View i = VotingButton._votingButton.get();
if (i == null || !VotingButton.shouldBeShown()) return;
i.setVisibility(VISIBLE);
i.bringToFront();
i.requestLayout();
i.invalidate();
private static void submitNewSegment() {
try {
ReVancedUtils.verifyOnMainThread();
final String uuid = SettingsEnum.SB_UUID.getString();
final long start = newSponsorSegmentStartMillis;
final long end = newSponsorSegmentEndMillis;
final String videoId = SegmentPlaybackController.getCurrentVideoId();
final long videoLength = VideoInformation.getCurrentVideoLength();
final SegmentCategory segmentCategory = newUserCreatedSegmentCategory;
if (start < 0 || end < 0 || start >= end || videoLength <= 0 || segmentCategory == null || videoId == null || uuid == null) {
LogHelper.printException(() -> "Unable to submit times, invalid parameters");
return;
}
clearUnsubmittedSegmentTimes();
ReVancedUtils.runOnBackgroundThread(() -> {
SBRequester.submitSegments(uuid, videoId, segmentCategory.key, start, end, videoLength);
SegmentPlaybackController.executeDownloadSegments(videoId);
});
} catch (Exception e) {
LogHelper.printException(() -> "Unable to submit segment", e);
}
}
public static void hideVoteButton() {
View i = VotingButton._votingButton.get();
if (i != null)
i.setVisibility(GONE);
}
public static void onMarkLocationClicked() {
try {
ReVancedUtils.verifyOnMainThread();
newSponsorSegmentDialogShownMillis = VideoInformation.getVideoTime();
@SuppressLint("DefaultLocale")
public static void onMarkLocationClicked(Context context) {
newSponsorSegmentDialogShownMillis = PlayerController.getLastKnownVideoTime();
new AlertDialog.Builder(context)
.setTitle(str("new_segment_title"))
.setMessage(str("new_segment_mark_time_as_question",
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
.setTitle(str("sb_new_segment_title"))
.setMessage(str("sb_new_segment_mark_time_as_question",
newSponsorSegmentDialogShownMillis / 60000,
newSponsorSegmentDialogShownMillis / 1000 % 60,
newSponsorSegmentDialogShownMillis % 1000))
.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton(str("new_segment_mark_start"), newSponsorSegmentDialogListener)
.setPositiveButton(str("new_segment_mark_end"), newSponsorSegmentDialogListener)
.setNegativeButton(str("sb_new_segment_mark_start"), newSponsorSegmentDialogListener)
.setPositiveButton(str("sb_new_segment_mark_end"), newSponsorSegmentDialogListener)
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "onMarkLocationClicked failure", ex);
}
}
@SuppressLint("DefaultLocale")
public static void onPublishClicked(Context context) {
if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
public static void onPublishClicked() {
try {
ReVancedUtils.verifyOnMainThread();
if (!newSponsorSegmentPreviewed) {
ReVancedUtils.showToastLong(str("sb_new_segment_preview_segment_first"));
} else if (newSponsorSegmentStartMillis >= 0 && newSponsorSegmentStartMillis < newSponsorSegmentEndMillis) {
long length = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000;
long start = (newSponsorSegmentStartMillis) / 1000;
long end = (newSponsorSegmentEndMillis) / 1000;
new AlertDialog.Builder(context)
.setTitle(str("new_segment_confirm_title"))
.setMessage(str("new_segment_confirm_content",
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
.setTitle(str("sb_new_segment_confirm_title"))
.setMessage(str("sb_new_segment_confirm_content",
start / 60, start % 60,
end / 60, end % 60,
length / 60, length % 60))
@ -315,29 +274,52 @@ public abstract class SponsorBlockUtils {
.setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener)
.show();
} else {
Toast.makeText(context, str("new_segment_mark_locations_first"), Toast.LENGTH_SHORT).show();
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
}
} catch (Exception ex) {
LogHelper.printException(() -> "onPublishClicked failure", ex);
}
}
public static void onVotingClicked(final Context context) {
if (sponsorSegmentsOfCurrentVideo == null || sponsorSegmentsOfCurrentVideo.length == 0) {
Toast.makeText(context.getApplicationContext(), str("vote_no_segments"), Toast.LENGTH_SHORT).show();
public static void onVotingClicked(@NonNull Context context) {
try {
ReVancedUtils.verifyOnMainThread();
SponsorSegment[] currentSegments = SegmentPlaybackController.getSegmentsOfCurrentVideo();
if (currentSegments == null || currentSegments.length == 0) {
// button is hidden if no segments exist.
// But if prior video had segments, and current video does not,
// then the button persists until the overlay fades out (this is intentional, as abruptly hiding the button is jarring)
ReVancedUtils.showToastShort(str("sb_vote_no_segments"));
return;
}
int segmentAmount = sponsorSegmentsOfCurrentVideo.length;
List<CharSequence> titles = new ArrayList<>(segmentAmount); // I've replaced an array with a list to prevent null elements in the array as unsubmitted segments get filtered out
for (int i = 0; i < segmentAmount; i++) {
SponsorSegment segment = sponsorSegmentsOfCurrentVideo[i];
if (segment.category == SponsorBlockSettings.SegmentInfo.UNSUBMITTED) {
// use same time formatting as shown in the video player
final long currentVideoLength = VideoInformation.getCurrentVideoLength();
final String formatPattern;
if (currentVideoLength < (10 * 60 * 1000)) {
formatPattern = "m:ss"; // less than 10 minutes
} else if (currentVideoLength < (60 * 60 * 1000)) {
formatPattern = "mm:ss"; // less than 1 hour
} else if (currentVideoLength < (10 * 60 * 60 * 1000)) {
formatPattern = "H:mm:ss"; // less than 10 hours
} else {
formatPattern = "HH:mm:ss"; // why is this on YouTube
}
voteSegmentTimeFormatter.applyPattern(formatPattern);
final int numberOfSegments = currentSegments.length;
List<CharSequence> titles = new ArrayList<>(numberOfSegments);
for (int i = 0; i < numberOfSegments; i++) {
SponsorSegment segment = currentSegments[i];
if (segment.category == SegmentCategory.UNSUBMITTED) {
continue;
}
String start = dateFormatter.format(new Date(segment.start));
String end = dateFormatter.format(new Date(segment.end));
String start = voteSegmentTimeFormatter.format(new Date(segment.start));
String end = voteSegmentTimeFormatter.format(new Date(segment.end));
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br> %s to %s",
segment.category.color, segment.category.title, start, end));
if (i + 1 != segmentAmount) // prevents trailing new line after last segment
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
htmlBuilder.append("<br>");
titles.add(Html.fromHtml(htmlBuilder.toString()));
}
@ -345,297 +327,109 @@ public abstract class SponsorBlockUtils {
new AlertDialog.Builder(context)
.setItems(titles.toArray(new CharSequence[0]), segmentVoteClickListener)
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "onVotingClicked failure", ex);
}
}
private static void onNewCategorySelect(final SponsorSegment segment, Context context) {
final SponsorBlockSettings.SegmentInfo[] values = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) {
try {
ReVancedUtils.verifyOnMainThread();
final SegmentCategory[] values = SegmentCategory.valuesWithoutUnsubmitted();
CharSequence[] titles = new CharSequence[values.length];
for (int i = 0; i < values.length; i++) {
titles[i] = values[i].getTitleWithDot();
titles[i] = values[i].getTitleWithColorDot();
}
new AlertDialog.Builder(context)
.setTitle(str("new_segment_choose_category"))
.setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), values[which].key))
.setTitle(str("sb_new_segment_choose_category"))
.setItems(titles, (dialog, which) -> SBRequester.voteToChangeCategoryOnBackgroundThread(segment, values[which]))
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "onNewCategorySelect failure", ex);
}
}
@SuppressLint("DefaultLocale")
public static void onPreviewClicked(Context context) {
public static void onPreviewClicked() {
try {
ReVancedUtils.verifyOnMainThread();
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;
VideoInformation.seekTo(newSponsorSegmentStartMillis - 2500);
final SponsorSegment[] original = SegmentPlaybackController.getSegmentsOfCurrentVideo();
final SponsorSegment[] segments = original == null ? new SponsorSegment[1] : Arrays.copyOf(original, original.length + 1);
segments[segments.length - 1] = new SponsorSegment(newSponsorSegmentStartMillis, newSponsorSegmentEndMillis,
SponsorBlockSettings.SegmentInfo.UNSUBMITTED, null, false);
segments[segments.length - 1] = new SponsorSegment(SegmentCategory.UNSUBMITTED, null,
newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, false);
Arrays.sort(segments);
sponsorSegmentsOfCurrentVideo = segments;
SegmentPlaybackController.setSegmentsOfCurrentVideo(segments);
} else {
Toast.makeText(context, str("new_segment_mark_locations_first"), Toast.LENGTH_SHORT).show();
ReVancedUtils.showToastShort(str("sb_new_segment_mark_locations_first"));
}
} catch (Exception ex) {
LogHelper.printException(() -> "onPreviewClicked failure", ex);
}
}
@SuppressLint("DefaultLocale")
public static void onEditByHandClicked(Context context) {
new AlertDialog.Builder(context)
.setTitle(str("new_segment_edit_by_hand_title"))
.setMessage(str("new_segment_edit_by_hand_content"))
.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton(str("new_segment_mark_start"), editByHandDialogListener)
.setPositiveButton(str("new_segment_mark_end"), editByHandDialogListener)
.show();
}
public static void notifyShareBtnVisibilityChanged(View v) {
if (v.getId() != shareBtnId || !/*SponsorBlockSettings.isAddNewSegmentEnabled*/false)
static void sendViewRequestAsync(@NonNull SponsorSegment segment) {
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
return;
// if (VERBOSE)
// LogH(TAG, "VISIBILITY CHANGED of view " + v);
ImageView sponsorBtn = ShieldButton._shieldBtn.get();
if (sponsorBtn != null) {
sponsorBtn.setVisibility(v.getVisibility());
}
segment.recordedAsSkipped = true;
final long totalTimeSkipped = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.getLong() + segment.length();
SettingsEnum.SB_SKIPPED_SEGMENTS_TIME_SAVED.saveValue(totalTimeSkipped);
SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.saveValue(SettingsEnum.SB_SKIPPED_SEGMENTS_NUMBER_SKIPPED.getInt() + 1);
if (SettingsEnum.SB_TRACK_SKIP_COUNT.getBoolean()) {
ReVancedUtils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segment));
}
}
public static String appendTimeWithoutSegments(String totalTime) {
public static void onEditByHandClicked() {
try {
if (videoHasSegments && (SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()) && !TextUtils.isEmpty(totalTime) && getCurrentVideoLength() > 1) {
if (timeWithoutSegments.isEmpty()) {
timeWithoutSegments = getTimeWithoutSegments(sponsorSegmentsOfCurrentVideo);
}
return totalTime + timeWithoutSegments;
}
ReVancedUtils.verifyOnMainThread();
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
.setTitle(str("sb_new_segment_edit_by_hand_title"))
.setMessage(str("sb_new_segment_edit_by_hand_content"))
.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton(str("sb_new_segment_mark_start"), editByHandDialogListener)
.setPositiveButton(str("sb_new_segment_mark_end"), editByHandDialogListener)
.show();
} catch (Exception ex) {
LogHelper.printException(() -> "appendTimeWithoutSegments failure", ex);
}
return totalTime;
}
public static String getTimeWithoutSegments(SponsorSegment[] sponsorSegmentsOfCurrentVideo) {
long currentVideoLength = getCurrentVideoLength();
if (!(SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean()) || sponsorSegmentsOfCurrentVideo == null || currentVideoLength <= 1) {
return "";
}
long timeWithoutSegments = currentVideoLength + 500; // YouTube:tm:
for (SponsorSegment segment : sponsorSegmentsOfCurrentVideo) {
timeWithoutSegments -= segment.end - segment.start;
}
long hours = timeWithoutSegments / 3600000;
long minutes = (timeWithoutSegments / 60000) % 60;
long seconds = (timeWithoutSegments / 1000) % 60;
String format = (hours > 0 ? "%d:%02" : "%") + "d:%02d"; // mmLul
String formatted = hours > 0 ? String.format(format, hours, minutes, seconds) : String.format(format, minutes, seconds);
return String.format(" (%s)", formatted);
}
public static void playerTypeChanged(PlayerType playerType) {
try {
if (videoHasSegments && (playerType == PlayerType.NONE)) {
PlayerController.setCurrentVideoId(null);
}
} catch (Exception ex) {
LogHelper.printException(() -> "Player type changed caused a crash.", ex);
LogHelper.printException(() -> "onEditByHandClicked failure", ex);
}
}
public static String formatColorString(int color) {
return String.format("#%06X", color);
public static String getTimeSavedString(long totalSecondsSaved) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Duration duration = Duration.ofSeconds(totalSecondsSaved);
final long hoursSaved = duration.toHours();
final long minutesSaved = duration.toMinutes() % 60;
if (hoursSaved > 0) {
return str("sb_stats_saved_hour_format", hoursSaved, minutesSaved);
}
@SuppressWarnings("deprecation")
public static void addUserStats(PreferenceCategory category, Preference loadingPreference, UserStats stats) {
category.removePreference(loadingPreference);
Context context = category.getContext();
String minutesStr = str("minutes");
{
EditTextPreference preference = new EditTextPreference(context);
category.addPreference(preference);
String userName = stats.getUserName();
preference.setTitle(fromHtml(str("stats_username", userName)));
preference.setSummary(str("stats_username_change"));
preference.setText(userName);
preference.setOnPreferenceChangeListener((preference1, newUsername) -> {
appContext = new WeakReference<>(context.getApplicationContext());
SBRequester.setUsername((String) newUsername, preference, toastRunnable);
return false;
});
final long secondsSaved = duration.getSeconds() % 60;
if (minutesSaved > 0) {
return str("sb_stats_saved_minute_format", minutesSaved, secondsSaved);
}
{
Preference preference = new Preference(context);
category.addPreference(preference);
String formatted = FORMATTER.format(stats.getSegmentCount());
preference.setTitle(fromHtml(str("stats_submissions", formatted)));
preference.setSelectable(false);
}
{
Preference preference = new Preference(context);
category.addPreference(preference);
String formatted = FORMATTER.format(stats.getViewCount());
double saved = stats.getMinutesSaved();
int hoursSaved = (int) (saved / 60);
double minutesSaved = saved % 60;
String formattedSaved = String.format(SAVED_TEMPLATE, hoursSaved, minutesSaved, minutesStr);
preference.setTitle(fromHtml(str("stats_saved", formatted)));
preference.setSummary(fromHtml(str("stats_saved_sum", formattedSaved)));
preference.setOnPreferenceClickListener(preference1 -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
preference1.getContext().startActivity(i);
return false;
});
}
{
Preference preference = new Preference(context);
category.addPreference(preference);
String formatted = FORMATTER.format(SettingsEnum.SB_SKIPPED_SEGMENTS.getInt());
long hoursSaved = SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() / 3600000;
double minutesSaved = (SettingsEnum.SB_SKIPPED_SEGMENTS_TIME.getLong() / 60000d) % 60;
String formattedSaved = String.format(SAVED_TEMPLATE, hoursSaved, minutesSaved, minutesStr);
preference.setTitle(fromHtml(str("stats_self_saved", formatted)));
preference.setSummary(fromHtml(str("stats_self_saved_sum", formattedSaved)));
preference.setSelectable(false);
}
}
public static void importSettings(String json, Context context) {
try {
JSONObject settingsJson = new JSONObject(json);
JSONObject barTypesObject = settingsJson.getJSONObject("barTypes");
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
SharedPreferences.Editor editor = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK).edit();
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
for (SponsorBlockSettings.SegmentInfo category : categories) {
String categoryKey = category.key;
JSONObject categoryObject = barTypesObject.getJSONObject(categoryKey);
String color = categoryObject.getString("color");
editor.putString(categoryKey + SponsorBlockSettings.CATEGORY_COLOR_SUFFIX, color);
editor.putString(categoryKey, SponsorBlockSettings.SegmentBehaviour.IGNORE.key);
}
for (int i = 0; i < categorySelectionsArray.length(); i++) {
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
String categoryKey = categorySelectionObject.getString("name");
SponsorBlockSettings.SegmentInfo category = SponsorBlockSettings.SegmentInfo.byCategoryKey(categoryKey);
if (category == null) {
continue;
}
int desktopKey = categorySelectionObject.getInt("option");
SponsorBlockSettings.SegmentBehaviour behaviour = SponsorBlockSettings.SegmentBehaviour.byDesktopKey(desktopKey);
editor.putString(category.key, behaviour.key);
}
SettingsEnum.SB_UUID.saveValue(settingsJson.getString("userID"));
SettingsEnum.SB_IS_VIP.saveValue(settingsJson.getBoolean("isVip"));
SettingsEnum.SB_API_URL.saveValue(settingsJson.getString("serverAddress"));
SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.saveValue(!settingsJson.getBoolean("dontShowNotice"));
SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.saveValue(settingsJson.getBoolean("showTimeWithSkips"));
SettingsEnum.SB_MIN_DURATION.saveValue(Float.valueOf(settingsJson.getString("minDuration")));
SettingsEnum.SB_COUNT_SKIPS.saveValue(settingsJson.getBoolean("trackViewCount"));
Toast.makeText(context, str("settings_import_successful"), Toast.LENGTH_SHORT).show();
} catch (Exception ex) {
LogHelper.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast
Toast.makeText(context, str("settings_import_failed"), Toast.LENGTH_SHORT).show();
}
}
public static String exportSettings(Context context) {
try {
JSONObject json = new JSONObject();
JSONObject barTypesObject = new JSONObject(); // categories' colors
JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
for (SponsorBlockSettings.SegmentInfo category : categories) {
JSONObject categoryObject = new JSONObject();
String categoryKey = category.key;
categoryObject.put("color", formatColorString(category.color));
barTypesObject.put(categoryKey, categoryObject);
int desktopKey = category.behaviour.desktopKey;
if (desktopKey != -1) {
JSONObject behaviorObject = new JSONObject();
behaviorObject.put("name", categoryKey);
behaviorObject.put("option", desktopKey);
categorySelectionsArray.put(behaviorObject);
}
}
json.put("userID", SettingsEnum.SB_UUID.getString());
json.put("isVip", SettingsEnum.SB_IS_VIP.getBoolean());
json.put("serverAddress", SettingsEnum.SB_API_URL.getString());
json.put("dontShowNotice", !SettingsEnum.SB_SHOW_TOAST_WHEN_SKIP.getBoolean());
json.put("showTimeWithSkips", SettingsEnum.SB_SHOW_TIME_WITHOUT_SEGMENTS.getBoolean());
json.put("minDuration", SettingsEnum.SB_MIN_DURATION.getFloat());
json.put("trackViewCount", SettingsEnum.SB_COUNT_SKIPS.getBoolean());
json.put("categorySelections", categorySelectionsArray);
json.put("barTypes", barTypesObject);
return json.toString();
} catch (Exception ex) {
LogHelper.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast
Toast.makeText(context, str("settings_export_failed"), Toast.LENGTH_SHORT).show();
return "";
}
}
public static boolean isSBButtonEnabled(Context context, String key) {
return SettingsEnum.SB_ENABLED.getBoolean() && SharedPrefHelper.getBoolean(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, key, false);
}
public enum VoteOption {
UPVOTE(str("vote_upvote"), false),
DOWNVOTE(str("vote_downvote"), true),
CATEGORY_CHANGE(str("vote_category"), true);
public final String title;
public final boolean shouldHighlight;
VoteOption(String title, boolean shouldHighlight) {
this.title = title;
this.shouldHighlight = shouldHighlight;
return str("sb_stats_saved_second_format", secondsSaved);
}
return "error"; // will never be reached. YouTube requires Android O or greater
}
private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener {
public boolean settingStart;
public WeakReference<EditText> editText;
boolean settingStart;
WeakReference<EditText> editText;
@SuppressLint("DefaultLocale")
@Override
public void onClick(DialogInterface dialog, int which) {
try {
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());
VideoInformation.getVideoTime() :
(Objects.requireNonNull(manualEditTimeFormatter.parse(editText.getText().toString())).getTime());
if (settingStart)
newSponsorSegmentStartMillis = Math.max(time, 0);
@ -646,10 +440,10 @@ public abstract class SponsorBlockUtils {
editByHandDialogListener.onClick(dialog, settingStart ?
DialogInterface.BUTTON_NEGATIVE :
DialogInterface.BUTTON_POSITIVE);
else
Toast.makeText(context.getApplicationContext(), str("new_segment_edit_by_hand_saved"), Toast.LENGTH_SHORT).show();
} catch (ParseException e) {
Toast.makeText(context.getApplicationContext(), str("new_segment_edit_by_hand_parse_error"), Toast.LENGTH_LONG).show();
ReVancedUtils.showToastLong(str("sb_new_segment_edit_by_hand_parse_error"));
} catch (Exception ex) {
LogHelper.printException(() -> "EditByHandSaveDialogListener failure", ex);
}
}
}

View File

@ -1,90 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.content.Context;
import android.content.res.Resources;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import app.revanced.integrations.sponsorblock.player.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
public class SwipeHelper {
static FrameLayout _frameLayout;
public static boolean isTabletMode;
public static ViewGroup nextGenWatchLayout;
public static void SetFrameLayout(Object obj) {
try {
_frameLayout = (FrameLayout) obj;
Context appContext = ReVancedUtils.getContext();
if (ReVancedUtils.isTablet(appContext) || SharedPrefHelper.getBoolean(SharedPrefHelper.SharedPrefNames.YOUTUBE, "pref_swipe_tablet", false)) {
isTabletMode = true;
}
} catch (Exception e) {
LogHelper.printException(() -> "Unable to set FrameLayout", e);
}
}
public static void setNextGenWatchLayout(Object obj) {
try {
nextGenWatchLayout = (ViewGroup) obj;
} catch (Exception e) {
LogHelper.printException(() -> "Unable to set _nextGenWatchLayout", e);
}
}
public static boolean IsControlsShown() {
FrameLayout frameLayout;
if (isTabletMode || (frameLayout = _frameLayout) == null || frameLayout.getVisibility() != View.VISIBLE) {
return false;
}
try {
} catch (Exception e) {
LogHelper.printException(() -> "Unable to get related_endscreen_results visibility", e);
}
if (_frameLayout.getChildCount() > 0) {
return _frameLayout.getChildAt(0).getVisibility() == View.VISIBLE;
}
refreshLayout();
return false;
}
private static void refreshLayout() {
View findViewById;
try {
if (isWatchWhileFullScreen() && (findViewById = nextGenWatchLayout.findViewById(getIdentifier())) != null) {
_frameLayout = (FrameLayout) findViewById.getParent();
LogHelper.printDebug(() -> "related_endscreen_results refreshed");
}
} catch (Exception e) {
LogHelper.printException(() -> "Unable to refresh related_endscreen_results layout", e);
}
}
private static boolean isWatchWhileFullScreen() {
if (ReVancedUtils.getPlayerType() == null) {
return false;
}
return ReVancedUtils.getPlayerType() == PlayerType.WATCH_WHILE_FULLSCREEN;
}
private static String getViewMessage(View view) {
try {
String resourceName = view.getResources() != null ? view.getId() != 0 ? view.getResources().getResourceName(view.getId()) : "no_id" : "no_resources";
return "[" + view.getClass().getSimpleName() + "] " + resourceName + "\n";
} catch (Resources.NotFoundException unused) {
return "[" + view.getClass().getSimpleName() + "] name_not_found\n";
}
}
private static int getIdentifier() {
Context appContext = ReVancedUtils.getContext();
assert appContext != null;
return appContext.getResources().getIdentifier("related_endscreen_results", "id", appContext.getPackageName());
}
}

View File

@ -1,118 +0,0 @@
package app.revanced.integrations.sponsorblock;
import android.content.Context;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import static app.revanced.integrations.sponsorblock.PlayerController.getCurrentVideoLength;
import static app.revanced.integrations.sponsorblock.PlayerController.getLastKnownVideoTime;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class VotingButton {
static RelativeLayout _youtubeControlsLayout;
static WeakReference<ImageView> _votingButton = new WeakReference<>(null);
static int fadeDurationFast;
static int fadeDurationScheduled;
static Animation fadeIn;
static Animation fadeOut;
static boolean isShowing;
public static void initialize(Object viewStub) {
try {
LogHelper.printDebug(() -> "initializing voting button");
_youtubeControlsLayout = (RelativeLayout) viewStub;
ImageView imageView = (ImageView) _youtubeControlsLayout
.findViewById(getIdentifier("voting_button", "id"));
if (imageView == null) {
LogHelper.printDebug(() -> "Couldn't find imageView with \"voting_button\"");
}
if (imageView == null) return;
imageView.setOnClickListener(SponsorBlockUtils.voteButtonListener);
_votingButton = new WeakReference<>(imageView);
// Animations
fadeDurationFast = getInteger("fade_duration_fast");
fadeDurationScheduled = getInteger("fade_duration_scheduled");
fadeIn = getAnimation("fade_in");
fadeIn.setDuration(fadeDurationFast);
fadeOut = getAnimation("fade_out");
fadeOut.setDuration(fadeDurationScheduled);
isShowing = true;
changeVisibilityImmediate(false);
} catch (Exception ex) {
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
}
}
public static void changeVisibilityImmediate(boolean visible) {
changeVisibility(visible, true);
}
public static void changeVisibilityNegatedImmediate(boolean visible) {
changeVisibility(!visible, true);
}
public static void changeVisibility(boolean visible) {
changeVisibility(visible, false);
}
public static void changeVisibility(boolean visible, boolean immediate) {
try {
if (isShowing == visible) return;
isShowing = visible;
ImageView iView = _votingButton.get();
if (_youtubeControlsLayout == null || iView == null) return;
if (visible && shouldBeShown()) {
if (getLastKnownVideoTime() >= getCurrentVideoLength()) {
return;
}
LogHelper.printDebug(() -> "Fading in");
iView.setVisibility(View.VISIBLE);
if (!immediate)
iView.startAnimation(fadeIn);
return;
}
if (iView.getVisibility() == View.VISIBLE) {
LogHelper.printDebug(() -> "Fading out");
if (!immediate)
iView.startAnimation(fadeOut);
iView.setVisibility(shouldBeShown() ? View.INVISIBLE : View.GONE);
}
} catch (Exception ex) {
LogHelper.printException(() -> "changeVisibility failure", ex);
}
}
static boolean shouldBeShown() {
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean();
}
//region Helpers
private static int getIdentifier(String name, String defType) {
Context context = ReVancedUtils.getContext();
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
private static int getInteger(String name) {
return ReVancedUtils.getContext().getResources().getInteger(getIdentifier(name, "integer"));
}
private static Animation getAnimation(String name) {
return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), getIdentifier(name, "anim"));
}
//endregion
}

View File

@ -0,0 +1,90 @@
package app.revanced.integrations.sponsorblock.objects;
import static app.revanced.integrations.utils.StringRef.sf;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.StringRef;
public enum CategoryBehaviour {
SKIP_AUTOMATICALLY("skip", 2, sf("sb_skip_automatically"), true),
// desktop does not have skip-once behavior. Key is unique to ReVanced
SKIP_AUTOMATICALLY_ONCE("skip-once", 4, sf("sb_skip_automatically_once"), true),
MANUAL_SKIP("manual-skip", 1, sf("sb_skip_showbutton"), false),
SHOW_IN_SEEKBAR("seekbar-only", 0, sf("sb_skip_seekbaronly"), false),
// Ignore is the default behavior if no desktop behavior key is present
IGNORE("ignore", 3, sf("sb_skip_ignore"), false);
@NonNull
public final String key;
public final int desktopKey;
@NonNull
public final StringRef name;
/**
* If the segment should skip automatically
*/
public final boolean skip;
CategoryBehaviour(String key,
int desktopKey,
StringRef name,
boolean skip) {
this.key = Objects.requireNonNull(key);
this.desktopKey = desktopKey;
this.name = Objects.requireNonNull(name);
this.skip = skip;
}
@Nullable
public static CategoryBehaviour byStringKey(@NonNull String key) {
for (CategoryBehaviour behaviour : values()){
if (behaviour.key.equals(key)) {
return behaviour;
}
}
return null;
}
@Nullable
public static CategoryBehaviour byDesktopKey(int desktopKey) {
for (CategoryBehaviour behaviour : values()) {
if (behaviour.desktopKey == desktopKey) {
return behaviour;
}
}
return null;
}
private static String[] behaviorKeys;
private static String[] behaviorNames;
private static void createNameAndKeyArrays() {
ReVancedUtils.verifyOnMainThread();
CategoryBehaviour[] behaviours = values();
behaviorKeys = new String[behaviours.length];
behaviorNames = new String[behaviours.length];
for (int i = 0, length = behaviours.length; i < length; i++) {
CategoryBehaviour behaviour = behaviours[i];
behaviorKeys[i] = behaviour.key;
behaviorNames[i] = behaviour.name.toString();
}
}
public static String[] getBehaviorNames() {
if (behaviorNames == null) {
createNameAndKeyArrays();
}
return behaviorNames;
}
public static String[] getBehaviorKeys() {
if (behaviorKeys == null) {
createNameAndKeyArrays();
}
return behaviorKeys;
}
}

View File

@ -1,124 +0,0 @@
package app.revanced.integrations.sponsorblock.objects;
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.formatColorString;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.preference.ListPreference;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;
import android.widget.Toast;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
@SuppressWarnings("deprecation")
public class EditTextListPreference extends ListPreference {
private EditText mEditText;
private int mClickedDialogEntryIndex;
public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public EditTextListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EditTextListPreference(Context context) {
super(context);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
SponsorBlockSettings.SegmentInfo category = getCategoryBySelf();
mEditText = new EditText(builder.getContext());
mEditText.setInputType(InputType.TYPE_CLASS_TEXT);
mEditText.setText(formatColorString(category.color));
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
try {
Color.parseColor(s.toString()); // validation
getDialog().setTitle(Html.fromHtml(String.format("<font color=\"%s\">⬤</font> %s", s, category.title)));
} catch (Exception ex) {
}
}
});
builder.setView(mEditText);
builder.setTitle(category.getTitleWithDot());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
});
builder.setNeutralButton(str("reset"), (dialog, which) -> {
//EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
int defaultColor = category.defaultColor;
category.setColor(defaultColor);
Toast.makeText(getContext().getApplicationContext(), str("color_reset"), Toast.LENGTH_SHORT).show();
getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(defaultColor)).apply();
reformatTitle();
});
builder.setNegativeButton(android.R.string.cancel, null);
mClickedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
}
String colorString = mEditText.getText().toString();
SponsorBlockSettings.SegmentInfo category = getCategoryBySelf();
if (colorString.equals(formatColorString(category.color))) {
return;
}
Context applicationContext = getContext().getApplicationContext();
try {
int color = Color.parseColor(colorString);
category.setColor(color);
Toast.makeText(applicationContext, str("color_changed"), Toast.LENGTH_SHORT).show();
getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(color)).apply();
reformatTitle();
} catch (Exception ex) {
Toast.makeText(applicationContext, str("color_invalid"), Toast.LENGTH_SHORT).show();
}
}
}
private SponsorBlockSettings.SegmentInfo getCategoryBySelf() {
return SponsorBlockSettings.SegmentInfo.byCategoryKey(getKey());
}
private String getColorPreferenceKey() {
return getKey() + SponsorBlockSettings.CATEGORY_COLOR_SUFFIX;
}
private void reformatTitle() {
this.setTitle(getCategoryBySelf().getTitleWithDot());
}
}

View File

@ -0,0 +1,305 @@
package app.revanced.integrations.sponsorblock.objects;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.IGNORE;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
import static app.revanced.integrations.utils.StringRef.sf;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.SharedPrefHelper;
import app.revanced.integrations.utils.StringRef;
public enum SegmentCategory {
SPONSOR("sponsor", sf("sb_segments_sponsor"), sf("sb_segments_sponsor_sum"), sf("sb_skip_button_sponsor"), sf("sb_skipped_sponsor"),
SKIP_AUTOMATICALLY, 0x00D400),
SELF_PROMO("selfpromo", sf("sb_segments_selfpromo"), sf("sb_segments_selfpromo_sum"), sf("sb_skip_button_selfpromo"), sf("sb_skipped_selfpromo"),
SKIP_AUTOMATICALLY, 0xFFFF00),
INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"),
SKIP_AUTOMATICALLY, 0xCC00FF),
INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"),
sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"),
sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"),
MANUAL_SKIP, 0x00FFFF),
OUTRO("outro", sf("sb_segments_outro"), sf("sb_segments_outro_sum"), sf("sb_skip_button_outro"), sf("sb_skipped_outro"),
MANUAL_SKIP, 0x0202ED),
PREVIEW("preview", sf("sb_segments_preview"), sf("sb_segments_preview_sum"),
sf("sb_skip_button_preview_beginning"), sf("sb_skip_button_preview_middle"), sf("sb_skip_button_preview_end"),
sf("sb_skipped_preview_beginning"), sf("sb_skipped_preview_middle"), sf("sb_skipped_preview_end"),
IGNORE, 0x008FD6),
FILLER("filler", sf("sb_segments_filler"), sf("sb_segments_filler_sum"), sf("sb_skip_button_filler"), sf("sb_skipped_filler"),
IGNORE, 0x7300FF),
MUSIC_OFFTOPIC("music_offtopic", sf("sb_segments_nomusic"), sf("sb_segments_nomusic_sum"), sf("sb_skip_button_nomusic"), sf("sb_skipped_nomusic"),
MANUAL_SKIP, 0xFF9900),
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("sb_skip_button_unsubmitted"), sf("sb_skipped_unsubmitted"),
SKIP_AUTOMATICALLY, 0xFFFFFF);
private static final SegmentCategory[] mValuesWithoutUnsubmitted = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
INTERACTION,
INTRO,
OUTRO,
PREVIEW,
FILLER,
MUSIC_OFFTOPIC,
};
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * mValuesWithoutUnsubmitted.length);
private static final String COLOR_PREFERENCE_KEY_SUFFIX = "_color";
/**
* Categories currently enabled, formatted for an API call
*/
public static String sponsorBlockAPIFetchCategories = "[]";
static {
for (SegmentCategory value : mValuesWithoutUnsubmitted)
mValuesMap.put(value.key, value);
}
@NonNull
public static SegmentCategory[] valuesWithoutUnsubmitted() {
return mValuesWithoutUnsubmitted;
}
@Nullable
public static SegmentCategory byCategoryKey(@NonNull String key) {
return mValuesMap.get(key);
}
public static void loadFromPreferences() {
SharedPreferences preferences = SharedPrefHelper.getPreferences(SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK);
LogHelper.printDebug(() -> "loadFromPreferences");
for (SegmentCategory category : valuesWithoutUnsubmitted()) {
category.load(preferences);
}
updateEnabledCategories();
}
/**
* Must be called if behavior of any category is changed
*/
public static void updateEnabledCategories() {
SegmentCategory[] categories = valuesWithoutUnsubmitted();
List<String> enabledCategories = new ArrayList<>(categories.length);
for (SegmentCategory category : categories) {
if (category.behaviour != CategoryBehaviour.IGNORE) {
enabledCategories.add(category.key);
}
}
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]";
if (enabledCategories.isEmpty())
sponsorBlockAPIFetchCategories = "[]";
else
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
}
@NonNull
public final String key;
@NonNull
public final StringRef title;
@NonNull
public final StringRef description;
/**
* Skip button text, if the skip occurs in the first quarter of the video
*/
@NonNull
public final StringRef skipButtonTextBeginning;
/**
* Skip button text, if the skip occurs in the middle half of the video
*/
@NonNull
public final StringRef skipButtonTextMiddle;
/**
* Skip button text, if the skip occurs in the last quarter of the video
*/
@NonNull
public final StringRef skipButtonTextEnd;
/**
* Skipped segment toast, if the skip occurred in the first quarter of the video
*/
@NonNull
public final StringRef skippedToastBeginning;
/**
* Skipped segment toast, if the skip occurred in the middle half of the video
*/
@NonNull
public final StringRef skippedToastMiddle;
/**
* Skipped segment toast, if the skip occurred in the last quarter of the video
*/
@NonNull
public final StringRef skippedToastEnd;
@NonNull
public final Paint paint;
public final int defaultColor;
/**
* If value is changed, then also call {@link #save(SharedPreferences.Editor)}
*/
public int color;
/**
* If value is changed, then also call {@link #updateEnabledCategories()}
*/
@NonNull
public CategoryBehaviour behaviour;
SegmentCategory(String key, StringRef title, StringRef description,
StringRef skipButtonText,
StringRef skippedToastText,
CategoryBehaviour defaultBehavior, int defaultColor) {
this(key, title, description,
skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText,
defaultBehavior, defaultColor);
}
SegmentCategory(String key, StringRef title, StringRef description,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
CategoryBehaviour defaultBehavior, int defaultColor) {
this.key = Objects.requireNonNull(key);
this.title = Objects.requireNonNull(title);
this.description = Objects.requireNonNull(description);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle);
this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd);
this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning);
this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle);
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviour = Objects.requireNonNull(defaultBehavior);
this.color = this.defaultColor = defaultColor;
this.paint = new Paint();
setColor(defaultColor);
}
/**
* Caller must also call {@link #updateEnabledCategories()}
*/
private void load(SharedPreferences preferences) {
String categoryColor = preferences.getString(key + COLOR_PREFERENCE_KEY_SUFFIX, null);
if (categoryColor == null) {
setColor(defaultColor);
} else {
setColor(categoryColor);
}
String behaviorString = preferences.getString(key, null);
if (behaviorString != null) {
CategoryBehaviour preferenceBehavior = CategoryBehaviour.byStringKey(behaviorString);
if (preferenceBehavior == null) {
LogHelper.printException(() -> "Unknown behavior: " + behaviorString); // should never happen
} else {
behaviour = preferenceBehavior;
}
}
}
/**
* Saves the current color and behavior.
* Calling code is responsible for calling {@link SharedPreferences.Editor#apply()}
*/
public void save(SharedPreferences.Editor editor) {
String colorString = (color == defaultColor)
? null // remove any saved preference, so default is used on the next load
: colorString();
editor.putString(key + COLOR_PREFERENCE_KEY_SUFFIX, colorString);
editor.putString(key, behaviour.key);
}
/**
* @return HTML color format string
*/
@NonNull
public String colorString() {
return String.format("#%06X", color);
}
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
setColor(Color.parseColor(colorString));
}
public void setColor(int color) {
color &= 0xFFFFFF;
this.color = color;
paint.setColor(color);
paint.setAlpha(255);
}
@NonNull
private static String getCategoryColorDotHTML(int color) {
color &= 0xFFFFFF;
return String.format("<font color=\"#%06X\">⬤</font>", color);
}
@NonNull
public static Spanned getCategoryColorDot(int color) {
return Html.fromHtml(getCategoryColorDotHTML(color));
}
@NonNull
public Spanned getCategoryColorDot() {
return getCategoryColorDot(color);
}
@NonNull
public Spanned getTitleWithColorDot() {
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
}
/**
* @param segmentStartTime video time the segment category started
* @param videoLength length of the video
* @return the skip button text
*/
@NonNull
public String getSkipButtonText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
return skipButtonTextBeginning.toString(); // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skipButtonTextBeginning.toString();
} else if (position < 0.75f) {
return skipButtonTextMiddle.toString();
}
return skipButtonTextEnd.toString();
}
/**
* @param segmentStartTime video time the segment category started
* @param videoLength length of the video
* @return 'skipped segment' toast message
*/
@NonNull
public String getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
return skippedToastBeginning.toString(); // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skippedToastBeginning.toString();
} else if (position < 0.75f) {
return skippedToastMiddle.toString();
}
return skippedToastEnd.toString();
}
}

View File

@ -0,0 +1,155 @@
package app.revanced.integrations.sponsorblock.objects;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.preference.ListPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import java.util.Objects;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class SegmentCategoryListPreference extends ListPreference {
private final SegmentCategory category;
private EditText mEditText;
private int mClickedDialogEntryIndex;
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
super(context);
this.category = Objects.requireNonNull(category);
setKey(category.key);
setEntries(CategoryBehaviour.getBehaviorNames());
setEntryValues(CategoryBehaviour.getBehaviorKeys());
setSummary(category.description.toString());
updateTitle();
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Context context = builder.getContext();
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(70, 0, 150, 0);
TableRow row = new TableRow(context);
TextView colorTextLabel = new TextView(context);
colorTextLabel.setText(str("sb_color_dot_label"));
row.addView(colorTextLabel);
TextView colorDotView = new TextView(context);
colorDotView.setText(category.getCategoryColorDot());
colorDotView.setPadding(30, 0, 30, 0);
row.addView(colorDotView);
mEditText = new EditText(context);
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
mEditText.setText(category.colorString());
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
try {
String colorString = s.toString();
if (!colorString.startsWith("#")) {
s.insert(0, "#"); // recursively calls back into this method
return;
}
if (colorString.length() > 7) {
s.delete(7, colorString.length());
return;
}
final int color = Color.parseColor(colorString);
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
} catch (IllegalArgumentException ex) {
// ignore
}
}
});
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(category.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
});
builder.setNeutralButton(str("sb_reset_color"), (dialog, which) -> {
try {
SharedPreferences.Editor editor = getSharedPreferences().edit();
category.setColor(category.defaultColor);
category.save(editor);
editor.apply();
updateTitle();
ReVancedUtils.showToastShort(str("sb_color_reset"));
} catch (Exception ex) {
LogHelper.printException(() -> "setNeutralButton failure", ex);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
mClickedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
} catch (Exception ex) {
LogHelper.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
@Override
protected void onDialogClosed(boolean positiveResult) {
try {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
category.behaviour = Objects.requireNonNull(CategoryBehaviour.byStringKey(value));
SegmentCategory.updateEnabledCategories();
}
String colorString = mEditText.getText().toString();
try {
final int color = Color.parseColor(colorString) & 0xFFFFFF;
if (color != category.color) {
category.setColor(color);
ReVancedUtils.showToastShort(str("sb_color_changed"));
}
} catch (IllegalArgumentException ex) {
ReVancedUtils.showToastShort(str("sb_color_invalid"));
}
// behavior is already saved, but color needs to be saved
SharedPreferences.Editor editor = getSharedPreferences().edit();
category.save(editor);
editor.apply();
updateTitle();
}
} catch (Exception ex) {
LogHelper.printException(() -> "onDialogClosed failure", ex);
}
}
private void updateTitle() {
setTitle(category.getTitleWithColorDot());
}
}

View File

@ -1,35 +1,130 @@
package app.revanced.integrations.sponsorblock.objects;
import static app.revanced.integrations.utils.StringRef.sf;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.MessageFormat;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.utils.StringRef;
public class SponsorSegment implements Comparable<SponsorSegment> {
public final long start;
public final long end;
public final SponsorBlockSettings.SegmentInfo category;
public final String UUID;
public final boolean isLocked;
public boolean didAutoSkipped = false;
public enum SegmentVote {
UPVOTE(sf("sb_vote_upvote"), 1,false),
DOWNVOTE(sf("sb_vote_downvote"), 0, true),
CATEGORY_CHANGE(sf("sb_vote_category"), -1, true); // apiVoteType is not used for category change
public SponsorSegment(long start, long end, SponsorBlockSettings.SegmentInfo category, String UUID, boolean isLocked) {
this.start = start;
this.end = end;
this.category = category;
this.UUID = UUID;
this.isLocked = isLocked;
@NonNull
public final StringRef title;
public final int apiVoteType;
public final boolean shouldHighlight;
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) {
this.title = title;
this.apiVoteType = apiVoteType;
this.shouldHighlight = shouldHighlight;
}
}
@NonNull
@Override
public String toString() {
return MessageFormat.format("SegmentInfo'{'start={0}, end={1}, category=''{2}'', locked={3}'}'", start, end, category, isLocked);
public final SegmentCategory category;
/**
* NULL if segment is unsubmitted
*/
@Nullable
public final String UUID;
public final long start;
public final long end;
public final boolean isLocked;
public boolean didAutoSkipped = false;
/**
* If this segment has been counted as 'skipped'
*/
public boolean recordedAsSkipped = false;
public SponsorSegment(@NonNull SegmentCategory category, @Nullable String UUID, long start, long end, boolean isLocked) {
this.category = category;
this.UUID = UUID;
this.start = start;
this.end = end;
this.isLocked = isLocked;
}
public boolean shouldAutoSkip() {
return category.behaviour.skip && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE);
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
*/
public boolean timeIsNearStart(long videoTime, long nearThreshold) {
return Math.abs(start - videoTime) <= nearThreshold;
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
*/
public boolean timeIsNearEnd(long videoTime, long nearThreshold) {
return Math.abs(end - videoTime) <= nearThreshold;
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment
* @return if the time parameter is within or close to this segment
*/
public boolean timeIsInsideOrNear(long videoTime, long nearThreshold) {
return (start - nearThreshold) <= videoTime && videoTime < (end + nearThreshold);
}
/**
* @return if the time parameter is outside this segment
*/
public boolean timeIsOutside(long videoTime) {
return start < videoTime || end <= videoTime;
}
/**
* @return if the segment is completely contained inside this segment
*/
public boolean containsSegment(SponsorSegment other) {
return start <= other.start && other.end <= end;
}
/**
* @return the length of this segment, in milliseconds. Always a positive number.
*/
public long length() {
return end - start;
}
/**
* @return 'skip segment' UI overlay button text
*/
@NonNull
public String getSkipButtonText() {
return category.getSkipButtonText(start, VideoInformation.getCurrentVideoLength());
}
/**
* @return 'skipped segment' toast message
*/
@NonNull
public String getSkippedToastText() {
return category.getSkippedToastText(start, VideoInformation.getCurrentVideoLength());
}
@Override
public int compareTo(SponsorSegment o) {
return (int) (this.start - o.start);
}
@NonNull
@Override
public String toString() {
return "SponsorSegment{"
+ "category=" + category
+ ", start=" + start
+ ", end=" + end
+ '}';
}
}

View File

@ -1,31 +1,45 @@
package app.revanced.integrations.sponsorblock.objects;
import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
/**
* SponsorBlock user stats
*/
public class UserStats {
private final String userName;
private final double minutesSaved;
private final int segmentCount;
private final int viewCount;
@NonNull
public final String publicUserId;
@NonNull
public final String userName;
/**
* "User reputation". Unclear how SB determines this value.
*/
public final float reputation;
public final int segmentCount;
public final int viewCount;
public final double minutesSaved;
public UserStats(String userName, double minutesSaved, int segmentCount, int viewCount) {
this.userName = userName;
this.minutesSaved = minutesSaved;
this.segmentCount = segmentCount;
this.viewCount = viewCount;
public UserStats(@NonNull JSONObject json) throws JSONException {
publicUserId = json.getString("userID");
userName = json.getString("userName");
reputation = (float)json.getDouble("reputation");
segmentCount = json.getInt("segmentCount");
viewCount = json.getInt("viewCount");
minutesSaved = json.getDouble("minutesSaved");
}
public String getUserName() {
return userName;
}
public double getMinutesSaved() {
return minutesSaved;
}
public int getSegmentCount() {
return segmentCount;
}
public int getViewCount() {
return viewCount;
@NonNull
@Override
public String toString() {
return "UserStats{"
+ "publicUserId='" + publicUserId + '\''
+ ", userName='" + userName + '\''
+ ", reputation=" + reputation
+ ", segmentCount=" + segmentCount
+ ", viewCount=" + viewCount
+ ", minutesSaved=" + minutesSaved
+ '}';
}
}

View File

@ -1,29 +0,0 @@
package app.revanced.integrations.sponsorblock.player;
import java.io.Serializable;
public class ChannelModel implements Serializable {
private String author;
private String channelId;
public ChannelModel(String author, String channelId) {
this.author = author;
this.channelId = channelId;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getChannelId() {
return channelId;
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
}

View File

@ -1,18 +0,0 @@
package app.revanced.integrations.sponsorblock.player;
public enum PlayerType {
NONE,
HIDDEN,
WATCH_WHILE_MINIMIZED,
WATCH_WHILE_MAXIMIZED,
WATCH_WHILE_FULLSCREEN,
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
INLINE_MINIMAL,
VIRTUAL_REALITY_FULLSCREEN,
WATCH_WHILE_PICTURE_IN_PICTURE;
}

View File

@ -1,40 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
import android.content.Context;
import app.revanced.integrations.utils.SharedPrefHelper;
public class ButtonVisibility {
public static Visibility getButtonVisibility(String key) {
return getButtonVisibility(key, SharedPrefHelper.SharedPrefNames.YOUTUBE);
}
public static Visibility getButtonVisibility(String key, SharedPrefHelper.SharedPrefNames name) {
String value = SharedPrefHelper.getString(name, key, null);
if (value == null || value.isEmpty()) return Visibility.NONE;
switch (value.toUpperCase()) {
case "PLAYER":
return Visibility.PLAYER;
case "BUTTON_CONTAINER":
return Visibility.BUTTON_CONTAINER;
case "BOTH":
return Visibility.BOTH;
default:
return Visibility.NONE;
}
}
public static boolean isVisibleInContainer(String key) {
return isVisibleInContainer(getButtonVisibility(key));
}
public static boolean isVisibleInContainer(String key, SharedPrefHelper.SharedPrefNames name) {
return isVisibleInContainer(getButtonVisibility(key, name));
}
public static boolean isVisibleInContainer(Visibility visibility) {
return visibility == Visibility.BOTH || visibility == Visibility.BUTTON_CONTAINER;
}
}

View File

@ -1,151 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.sponsorblock.NewSegmentHelperLayout;
import app.revanced.integrations.sponsorblock.PlayerController;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
public class NewSegmentLayout extends FrameLayout {
private LinearLayout newSegmentContainer;
public int defaultBottomMargin;
public int ctaBottomMargin;
public ImageButton rewindButton;
public ImageButton forwardButton;
public ImageButton adjustButton;
public ImageButton compareButton;
public ImageButton editButton;
public ImageButton publishButton;
private int rippleEffectId;
public NewSegmentLayout(Context context) {
super(context);
this.initialize(context);
}
public NewSegmentLayout(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
this.initialize(context);
}
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr) {
super(context, attributeSet, defStyleAttr);
this.initialize(context);
}
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
super(context, attributeSet, defStyleAttr, defStyleRes);
this.initialize(context);
}
private final void initialize(Context context) {
LayoutInflater.from(context).inflate(getIdentifier(context, "new_segment", "layout"), this, true);
Resources resources = context.getResources();
TypedValue rippleEffect = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true);
rippleEffectId = rippleEffect.resourceId;
this.newSegmentContainer = (LinearLayout) this.findViewById(getIdentifier(context, "new_segment_container", "id"));
this.rewindButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_rewind", "id"));
if (this.rewindButton != null) {
setClickEffect(this.rewindButton);
this.rewindButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Rewind button clicked");
PlayerController.skipRelativeMilliseconds(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
}
});
}
this.forwardButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_forward", "id"));
if (this.forwardButton != null) {
setClickEffect(this.forwardButton);
this.forwardButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Forward button clicked");
PlayerController.skipRelativeMilliseconds(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
}
});
}
this.adjustButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_adjust", "id"));
if (this.adjustButton != null) {
setClickEffect(this.adjustButton);
this.adjustButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Adjust button clicked");
SponsorBlockUtils.onMarkLocationClicked(NewSegmentHelperLayout.context);
}
});
}
this.compareButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_compare", "id"));
if (this.compareButton != null) {
setClickEffect(this.compareButton);
this.compareButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Compare button clicked");
SponsorBlockUtils.onPreviewClicked(NewSegmentHelperLayout.context);
}
});
}
this.editButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_edit", "id"));
if (this.editButton != null) {
setClickEffect(this.editButton);
this.editButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Edit button clicked");
SponsorBlockUtils.onEditByHandClicked(NewSegmentHelperLayout.context);
}
});
}
this.publishButton = (ImageButton) this.findViewById(getIdentifier(context, "new_segment_publish", "id"));
if (this.publishButton != null) {
setClickEffect(this.publishButton);
this.publishButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Publish button clicked");
SponsorBlockUtils.onPublishClicked(NewSegmentHelperLayout.context);
}
});
}
this.defaultBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "brand_interaction_default_bottom_margin", "dimen"));
this.ctaBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "brand_interaction_cta_bottom_margin", "dimen"));
}
private void setClickEffect(ImageButton btn) {
btn.setBackgroundResource(rippleEffectId);
RippleDrawable rippleDrawable = (RippleDrawable) btn.getBackground();
int[][] states = new int[][]{new int[]{android.R.attr.state_enabled}};
int[] colors = new int[]{0x33ffffff}; // sets the ripple color to white
ColorStateList colorStateList = new ColorStateList(states, colors);
rippleDrawable.setColor(colorStateList);
}
private int getIdentifier(Context context, String name, String defType) {
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
}

View File

@ -1,124 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.sponsorblock.PlayerController;
public class SkipSponsorButton extends FrameLayout {
public CharSequence skipSponsorTextViewText;
public CharSequence skipSponsorText;
public ImageView skipSponsorButtonIcon;
public TextView skipSponsorTextView;
public int currentTextColor;
public int invertedButtonForegroundColor;
public int backgroundColor;
public int invertedBackgroundColor;
public ColorDrawable backgroundColorDrawable;
public int defaultBottomMargin;
public int ctaBottomMargin;
private LinearLayout skipSponsorBtnContainer;
private final Paint background;
private final Paint border;
private boolean highContrast = true;
public SkipSponsorButton(Context context) {
super(context);
this.background = new Paint();
this.border = new Paint();
this.initialize(context);
}
public SkipSponsorButton(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
this.background = new Paint();
this.border = new Paint();
this.initialize(context);
}
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) {
super(context, attributeSet, defStyleAttr);
this.background = new Paint();
this.border = new Paint();
this.initialize(context);
}
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
super(context, attributeSet, defStyleAttr, defStyleRes);
this.background = new Paint();
this.border = new Paint();
this.initialize(context);
}
private final void initialize(Context context) {
LayoutInflater.from(context).inflate(getIdentifier(context, "skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
this.setMinimumHeight(this.getResources().getDimensionPixelSize(getIdentifier(context, "ad_skip_ad_button_min_height", "dimen"))); // dimen:ad_skip_ad_button_min_height
this.skipSponsorBtnContainer = (LinearLayout) this.findViewById(getIdentifier(context, "skip_sponsor_button_container", "id")); // id:skip_ad_button_container
this.skipSponsorButtonIcon = (ImageView) this.findViewById(getIdentifier(context, "skip_sponsor_button_icon", "id")); // id:skip_ad_button_icon
this.backgroundColor = getColor(context, getIdentifier(context, "skip_ad_button_background_color", "color")); // color:skip_ad_button_background_color
this.invertedBackgroundColor = getColor(context, getIdentifier(context, "skip_ad_button_inverted_background_color", "color")); // color:skip_ad_button_inverted_background_color
this.background.setColor(this.backgroundColor);
this.background.setStyle(Paint.Style.FILL);
int borderColor = getColor(context, getIdentifier(context, "skip_ad_button_border_color", "color")); // color:skip_ad_button_border_color
this.border.setColor(borderColor);
float borderWidth = this.getResources().getDimension(getIdentifier(context, "ad_skip_ad_button_border_width", "dimen")); // dimen:ad_skip_ad_button_border_width
this.border.setStrokeWidth(borderWidth);
this.border.setStyle(Paint.Style.STROKE);
TextView skipSponsorText = (TextView) this.findViewById(getIdentifier(context, "skip_sponsor_button_text", "id")); // id:skip_ad_button_text
this.skipSponsorTextView = skipSponsorText;
this.skipSponsorTextViewText = skipSponsorText.getText();
this.currentTextColor = this.skipSponsorTextView.getCurrentTextColor();
this.invertedButtonForegroundColor = getColor(context, getIdentifier(context, "skip_ad_button_inverted_foreground_color", "color")); // color:skip_ad_button_inverted_foreground_color
this.backgroundColorDrawable = new ColorDrawable(this.backgroundColor);
Resources resources = context.getResources();
this.defaultBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "skip_button_default_bottom_margin", "dimen")); // dimen:skip_button_default_bottom_margin
this.ctaBottomMargin = resources.getDimensionPixelSize(getIdentifier(context, "skip_button_cta_bottom_margin", "dimen")); // dimen:skip_button_cta_bottom_margin
this.skipSponsorText = resources.getText(getIdentifier(context, "skip_sponsor", "string")); // string:skip_ads "Skip ads"
this.skipSponsorBtnContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogHelper.printDebug(() -> "Skip button clicked");
PlayerController.onSkipSponsorClicked();
}
});
}
@Override // android.view.ViewGroup
protected final void dispatchDraw(Canvas canvas) {
int width = this.skipSponsorBtnContainer.getWidth();
int height = this.skipSponsorBtnContainer.getHeight();
int top = this.skipSponsorBtnContainer.getTop();
int left = this.skipSponsorBtnContainer.getLeft();
float floatLeft = (float) left;
float floatTop = (float) top;
float floatWidth = (float) (left + width);
float floatHeight = (float) (top + height);
canvas.drawRect(floatLeft, floatTop, floatWidth, floatHeight, this.background);
if (!this.highContrast) {
canvas.drawLines(new float[]{floatWidth, floatTop, floatLeft, floatTop, floatLeft, floatTop, floatLeft, floatHeight, floatLeft, floatHeight, floatWidth, floatHeight}, this.border);
}
super.dispatchDraw(canvas);
}
public static int getColor(Context context, int arg3) {
return context.getColor(arg3);
}
private int getIdentifier(Context context, String name, String defType) {
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
}

View File

@ -1,67 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public abstract class SlimButton implements View.OnClickListener {
public static int SLIM_METADATA_BUTTON_ID;
public final View view;
public final Context context;
private final ViewGroup container;
protected final ImageView button_icon;
protected final TextView button_text;
private boolean viewAdded = false;
static {
SLIM_METADATA_BUTTON_ID = ReVancedUtils.getIdentifier("slim_metadata_button", "layout");
}
public SlimButton(Context context, ViewGroup container, int id, boolean visible) {
LogHelper.printDebug(() -> "Adding button with id " + id + " and visibility of " + visible);
this.context = context;
this.container = container;
view = LayoutInflater.from(context).inflate(id, container, false);
button_icon = (ImageView) view.findViewById(ReVancedUtils.getIdentifier("button_icon", "id"));
button_text = (TextView) view.findViewById(ReVancedUtils.getIdentifier("button_text", "id"));
view.setOnClickListener(this);
setVisible(visible);
}
public void setVisible(boolean visible) {
try {
if (!viewAdded && visible) {
container.addView(view);
viewAdded = true;
} else if (viewAdded && !visible) {
container.removeView(view);
viewAdded = false;
}
setContainerVisibility();
} catch (Exception ex) {
LogHelper.printException(() -> "Error while changing button visibility", ex);
}
}
private void setContainerVisibility() {
if (container == null) return;
for (int i = 0; i < container.getChildCount(); i++) {
if (container.getChildAt(i).getVisibility() == View.VISIBLE) {
container.setVisibility(View.VISIBLE);
return;
}
}
container.setVisibility(View.GONE);
}
}

View File

@ -1,175 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import app.revanced.integrations.sponsorblock.player.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.sponsorblock.SwipeHelper;
public class SponsorBlockView {
static RelativeLayout inlineSponsorOverlay;
static ViewGroup _youtubeOverlaysLayout;
static WeakReference<SkipSponsorButton> _skipSponsorButton = new WeakReference<>(null);
static WeakReference<NewSegmentLayout> _newSegmentLayout = new WeakReference<>(null);
static boolean shouldShowOnPlayerType = true;
public static void initialize(Object viewGroup) {
try {
LogHelper.printDebug(() -> "initializing");
_youtubeOverlaysLayout = (ViewGroup) viewGroup;
addView();
} catch (Exception ex) {
LogHelper.printException(() -> "Unable to set ViewGroup", ex);
}
}
public static void showSkipButton() {
skipSponsorButtonVisibility(true);
}
public static void hideSkipButton() {
skipSponsorButtonVisibility(false);
}
public static void showNewSegmentLayout() {
newSegmentLayoutVisibility(true);
}
public static void hideNewSegmentLayout() {
newSegmentLayoutVisibility(false);
}
public static void playerTypeChanged(PlayerType playerType) {
try {
shouldShowOnPlayerType = (playerType == PlayerType.WATCH_WHILE_FULLSCREEN || playerType == PlayerType.WATCH_WHILE_MAXIMIZED);
if (playerType == PlayerType.WATCH_WHILE_FULLSCREEN) {
setSkipBtnMargins(true);
setNewSegmentLayoutMargins(true);
return;
}
setSkipBtnMargins(false);
setNewSegmentLayoutMargins(false);
} catch (Exception ex) {
LogHelper.printException(() -> "Player type changed caused a crash.", ex);
}
}
private static void addView() {
inlineSponsorOverlay = new RelativeLayout(ReVancedUtils.getContext());
setLayoutParams(inlineSponsorOverlay);
LayoutInflater.from(ReVancedUtils.getContext()).inflate(getIdentifier("inline_sponsor_overlay", "layout"), inlineSponsorOverlay);
_youtubeOverlaysLayout.addView(inlineSponsorOverlay, _youtubeOverlaysLayout.getChildCount() - 2);
SkipSponsorButton skipSponsorButton = (SkipSponsorButton) inlineSponsorOverlay.findViewById(getIdentifier("skip_sponsor_button", "id"));
_skipSponsorButton = new WeakReference<>(skipSponsorButton);
NewSegmentLayout newSegmentView = (NewSegmentLayout) inlineSponsorOverlay.findViewById(getIdentifier("new_segment_view", "id"));
_newSegmentLayout = new WeakReference<>(newSegmentView);
}
private static void setLayoutParams(RelativeLayout relativeLayout) {
relativeLayout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
}
private static void setSkipBtnMargins(boolean fullScreen) {
SkipSponsorButton skipSponsorButton = _skipSponsorButton.get();
if (skipSponsorButton == null) {
LogHelper.printException(() -> "Unable to setSkipBtnMargins");
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams();
if (params == null) {
LogHelper.printException(() -> "Unable to setSkipBtnMargins");
return;
}
params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin;
skipSponsorButton.setLayoutParams(params);
}
private static void skipSponsorButtonVisibility(boolean visible) {
SkipSponsorButton skipSponsorButton = _skipSponsorButton.get();
if (skipSponsorButton == null) {
LogHelper.printException(() -> "Unable to skipSponsorButtonVisibility");
return;
}
visible &= shouldShowOnPlayerType;
skipSponsorButton.setVisibility(visible ? View.VISIBLE : View.GONE);
bringLayoutToFront();
}
private static void setNewSegmentLayoutMargins(boolean fullScreen) {
NewSegmentLayout newSegmentLayout = _newSegmentLayout.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins");
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams();
if (params == null) {
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins");
return;
}
params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin;
newSegmentLayout.setLayoutParams(params);
}
private static void newSegmentLayoutVisibility(boolean visible) {
NewSegmentLayout newSegmentLayout = _newSegmentLayout.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "Unable to newSegmentLayoutVisibility");
return;
}
visible &= shouldShowOnPlayerType;
newSegmentLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
bringLayoutToFront();
}
private static void bringLayoutToFront() {
checkLayout();
inlineSponsorOverlay.bringToFront();
inlineSponsorOverlay.requestLayout();
inlineSponsorOverlay.invalidate();
}
private static void checkLayout() {
if (inlineSponsorOverlay.getHeight() == 0) {
ViewGroup watchLayout = SwipeHelper.nextGenWatchLayout;
if (watchLayout == null) {
LogHelper.printDebug(() -> "nextGenWatchLayout is null!");
return;
}
View layout = watchLayout.findViewById(getIdentifier("player_overlays", "id"));
if (layout == null) {
LogHelper.printDebug(() -> "player_overlays was not found for SB");
return;
}
initialize(layout);
LogHelper.printDebug(() -> "player_overlays refreshed for SB");
}
}
private static int getIdentifier(String name, String defType) {
Context context = ReVancedUtils.getContext();
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
}

View File

@ -1,26 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import app.revanced.integrations.utils.ReVancedUtils;
public class SponsorBlockVoting extends SlimButton {
public SponsorBlockVoting(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false);
initialize();
}
private void initialize() {
this.button_icon.setImageResource(ReVancedUtils.getIdentifier("revanced_sb_voting", "drawable"));
this.button_text.setText("SB Voting");
}
@Override
public void onClick(View view) {
Toast.makeText(ReVancedUtils.getContext(), "Nothing atm", Toast.LENGTH_SHORT).show();
}
}

View File

@ -1,8 +0,0 @@
package app.revanced.integrations.sponsorblock.player.ui;
public enum Visibility {
NONE,
PLAYER,
BUTTON_CONTAINER,
BOTH,
}

View File

@ -1,22 +1,17 @@
package app.revanced.integrations.sponsorblock.requests;
import static android.text.Html.fromHtml;
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.timeWithoutSegments;
import static app.revanced.integrations.sponsorblock.SponsorBlockUtils.videoHasSegments;
import static app.revanced.integrations.sponsorblock.StringRef.str;
import static app.revanced.integrations.utils.ReVancedUtils.runOnMainThread;
import static app.revanced.integrations.utils.StringRef.str;
import android.content.Context;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@ -25,207 +20,253 @@ import java.util.concurrent.TimeUnit;
import app.revanced.integrations.requests.Requester;
import app.revanced.integrations.requests.Route;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.PlayerController;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils.VoteOption;
import app.revanced.integrations.sponsorblock.objects.CategoryBehaviour;
import app.revanced.integrations.sponsorblock.objects.SegmentCategory;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment.SegmentVote;
import app.revanced.integrations.sponsorblock.objects.UserStats;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class SBRequester {
private static final String TIME_TEMPLATE = "%.3f";
/**
* TCP timeout
*/
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 7000;
/**
* HTTP response timeout
*/
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 10000;
/**
* Response code of a successful API call
*/
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
private SBRequester() {
}
public static synchronized SponsorSegment[] getSegments(String videoId) {
@NonNull
public static SponsorSegment[] getSegments(@NonNull String videoId) {
ReVancedUtils.verifyOffMainThread();
List<SponsorSegment> segments = new ArrayList<>();
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories);
int responseCode = connection.getResponseCode();
runVipCheck();
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SegmentCategory.sponsorBlockAPIFetchCategories);
final int responseCode = connection.getResponseCode();
if (responseCode == 200) {
// FIXME? should this use Requester#getJSONArray and not disconnect?
// HTTPURLConnection#disconnect says:
// disconnect if other requests to the server
// are unlikely in the near future.
JSONArray responseArray = Requester.parseJSONArrayAndDisconnect(connection);
int length = responseArray.length();
for (int i = 0; i < length; i++) {
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
JSONArray responseArray = Requester.parseJSONArray(connection);
final long minSegmentDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000);
for (int i = 0, length = responseArray.length(); i < length; i++) {
JSONObject obj = (JSONObject) responseArray.get(i);
JSONArray segment = obj.getJSONArray("segment");
long start = (long) (segment.getDouble(0) * 1000);
long end = (long) (segment.getDouble(1) * 1000);
long minDuration = (long) (SettingsEnum.SB_MIN_DURATION.getFloat() * 1000);
if ((end - start) < minDuration)
final long start = (long) (segment.getDouble(0) * 1000);
final long end = (long) (segment.getDouble(1) * 1000);
if ((end - start) < minSegmentDuration)
continue;
String category = obj.getString("category");
String categoryKey = obj.getString("category");
String uuid = obj.getString("UUID");
boolean locked = obj.getInt("locked") == 1;
SponsorBlockSettings.SegmentInfo segmentCategory = SponsorBlockSettings.SegmentInfo.byCategoryKey(category);
if (segmentCategory != null && segmentCategory.behaviour.showOnTimeBar) {
SponsorSegment sponsorSegment = new SponsorSegment(start, end, segmentCategory, uuid, locked);
SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(categoryKey);
if (segmentCategory == null) {
LogHelper.printException(() -> "Received unknown category: " + categoryKey); // should never happen
} else if (segmentCategory.behaviour != CategoryBehaviour.IGNORE) {
SponsorSegment sponsorSegment = new SponsorSegment(segmentCategory, uuid, start, end, locked);
segments.add(sponsorSegment);
}
}
if (!segments.isEmpty()) {
videoHasSegments = true;
timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0]));
LogHelper.printDebug(() -> {
StringBuilder builder = new StringBuilder("Downloaded segments:");
for (SponsorSegment segment : segments) {
builder.append('\n').append(segment);
}
return builder.toString();
});
runVipCheckInBackgroundIfNeeded();
} else if (responseCode == 404) {
// no segments are found. a normal response
LogHelper.printDebug(() -> "No segments found for video: " + videoId);
} else {
LogHelper.printException(() -> "getSegments failed with response code: " + responseCode,
null, str("sb_sponsorblock_connection_failure_status", responseCode));
connection.disconnect(); // something went wrong, might as well disconnect
}
connection.disconnect();
} catch (SocketTimeoutException ex) {
LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_timeout"));
} catch (Exception ex) {
LogHelper.printException(() -> "failed to get segments", ex);
LogHelper.printException(() -> "Failed to get segments", ex, str("sb_sponsorblock_connection_failure_generic"));
}
return segments.toArray(new SponsorSegment[0]);
}
public static void submitSegments(String videoId, String uuid, float startTime, float endTime, String category, Runnable toastRunnable) {
public static void submitSegments(@NonNull String userPrivateId, @NonNull String videoId, @NonNull String category,
long startTime, long endTime, long videoLength) {
ReVancedUtils.verifyOffMainThread();
try {
String start = String.format(Locale.US, TIME_TEMPLATE, startTime);
String end = String.format(Locale.US, TIME_TEMPLATE, endTime);
String duration = String.valueOf(PlayerController.getCurrentVideoLength() / 1000);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, videoId, uuid, start, end, category, duration);
int responseCode = connection.getResponseCode();
String start = String.format(Locale.US, TIME_TEMPLATE, startTime / 1000f);
String end = String.format(Locale.US, TIME_TEMPLATE, endTime / 1000f);
String duration = String.format(Locale.US, TIME_TEMPLATE, videoLength / 1000f);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, userPrivateId, videoId, category, start, end, duration);
final int responseCode = connection.getResponseCode();
final String messageToToast;
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("submit_succeeded");
case HTTP_STATUS_CODE_SUCCESS:
messageToToast = str("sb_submit_succeeded");
break;
case 409:
SponsorBlockUtils.messageToToast = str("submit_failed_duplicate");
messageToToast = str("sb_submit_failed_duplicate");
break;
case 403:
SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection));
messageToToast = str("sb_submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection));
break;
case 429:
SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit");
messageToToast = str("sb_submit_failed_rate_limit");
break;
case 400:
SponsorBlockUtils.messageToToast = str("submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection));
messageToToast = str("sb_submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection));
break;
default:
SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage());
messageToToast = str("sb_submit_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
runOnMainThread(toastRunnable);
connection.disconnect();
ReVancedUtils.showToastLong(messageToToast);
} catch (SocketTimeoutException ex) {
ReVancedUtils.showToastLong(str("sb_submit_failed_timeout"));
} catch (Exception ex) {
LogHelper.printException(() -> "failed to submit segments", ex);
}
}
public static void sendViewCountRequest(SponsorSegment segment) {
public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) {
ReVancedUtils.verifyOffMainThread();
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
connection.disconnect();
final int responseCode = connection.getResponseCode();
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
LogHelper.printDebug(() -> "Successfully sent view count for segment: " + segment);
} else {
LogHelper.printDebug(() -> "Failed to sent view count for segment: " + segment.UUID
+ " responseCode: " + responseCode); // debug level, no toast is shown
}
} catch (IOException ex) {
LogHelper.printInfo(() -> "Failed to send view count", ex); // do not show a toast
} catch (Exception ex) {
LogHelper.printException(() -> "failed to send view count request", ex);
LogHelper.printException(() -> "Failed to send view count request", ex); // should never happen
}
}
public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, String... args) {
public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) {
voteOrRequestCategoryChange(segment, voteOption, null);
}
public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) {
voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor);
}
private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) {
ReVancedUtils.runOnBackgroundThread(() -> {
try {
String segmentUuid = segment.UUID;
String uuid = SettingsEnum.SB_UUID.getString();
String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0);
HttpURLConnection connection = voteOption == VoteOption.CATEGORY_CHANGE
? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, segmentUuid, uuid, args[0])
: getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, segmentUuid, uuid, vote);
int responseCode = connection.getResponseCode();
HttpURLConnection connection = (voteOption == SegmentVote.CATEGORY_CHANGE)
? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, uuid, segmentUuid, categoryToVoteFor.key)
: getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, uuid, segmentUuid, String.valueOf(voteOption.apiVoteType));
final int responseCode = connection.getResponseCode();
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("vote_succeeded");
case HTTP_STATUS_CODE_SUCCESS:
LogHelper.printDebug(() -> "Vote success for segment: " + segment);
break;
case 403:
SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection));
ReVancedUtils.showToastLong(
str("sb_vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)));
break;
default:
SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage());
ReVancedUtils.showToastLong(
str("sb_vote_failed_unknown_error", responseCode, connection.getResponseMessage()));
break;
}
runOnMainThread(() -> Toast.makeText(context, SponsorBlockUtils.messageToToast, Toast.LENGTH_LONG).show());
connection.disconnect();
} catch (SocketTimeoutException ex) {
LogHelper.printException(() -> "failed to vote for segment", ex, str("sb_vote_failed_timeout"));
} catch (Exception ex) {
LogHelper.printException(() -> "failed to vote for segment", ex);
LogHelper.printException(() -> "failed to vote for segment", ex); // should never happen
}
});
}
public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) {
if (!SettingsEnum.SB_ENABLED.getBoolean()) {
loadingPreference.setTitle(str("stats_sb_disabled"));
return;
}
ReVancedUtils.runOnBackgroundThread(() -> {
/**
* @return NULL, if stats fetch failed
*/
@Nullable
public static UserStats retrieveUserStats() {
ReVancedUtils.verifyOffMainThread();
try {
JSONObject json = getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString());
UserStats stats = new UserStats(json.getString("userName"), json.getDouble("minutesSaved"), json.getInt("segmentCount"),
json.getInt("viewCount"));
runOnMainThread(() -> { // get back on main thread to modify UI elements
SponsorBlockUtils.addUserStats(category, loadingPreference, stats);
});
UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SettingsEnum.SB_UUID.getString()));
LogHelper.printDebug(() -> "user stats: " + stats);
return stats;
} catch (IOException ex) {
LogHelper.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
} catch (Exception ex) {
LogHelper.printException(() -> "failed to retrieve user stats", ex);
LogHelper.printException(() -> "failure retrieving user stats", ex); // should never happen
}
});
return null;
}
public static void setUsername(String username, EditTextPreference preference, Runnable toastRunnable) {
ReVancedUtils.runOnBackgroundThread(() -> {
/**
* @return NULL if the call was successful. If unsuccessful, an error message is returned.
*/
@Nullable
public static String setUsername(@NonNull String username) {
ReVancedUtils.verifyOffMainThread();
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SettingsEnum.SB_UUID.getString(), username);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
SponsorBlockUtils.messageToToast = str("stats_username_changed");
runOnMainThread(() -> {
preference.setTitle(fromHtml(str("stats_username", username)));
preference.setText(username);
});
} else {
SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage());
final int responseCode = connection.getResponseCode();
String responseMessage = connection.getResponseMessage();
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
return null;
}
runOnMainThread(toastRunnable);
connection.disconnect();
} catch (Exception ex) {
LogHelper.printException(() -> "failed to set username", ex);
return str("sb_stats_username_change_unknown_error", responseCode, responseMessage);
} catch (Exception ex) { // should never happen
LogHelper.printInfo(() -> "failed to set username", ex); // do not toast
return str("sb_stats_username_change_unknown_error", 0, ex.getMessage());
}
});
}
public static void runVipCheck() {
public static void runVipCheckInBackgroundIfNeeded() {
long now = System.currentTimeMillis();
if (now < (SettingsEnum.SB_LAST_VIP_CHECK.getLong() + TimeUnit.DAYS.toMillis(3))) {
return;
}
ReVancedUtils.runOnBackgroundThread(() -> {
try {
JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SettingsEnum.SB_UUID.getString());
boolean vip = json.getBoolean("vip");
SettingsEnum.SB_IS_VIP.saveValue(vip);
SettingsEnum.SB_LAST_VIP_CHECK.saveValue(now);
} catch (IOException ex) {
LogHelper.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown
} catch (Exception ex) {
LogHelper.printException(() -> "failed to check VIP", ex);
LogHelper.printException(() -> "Failed to check VIP", ex); // should never happen
}
});
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(SettingsEnum.SB_API_URL.getString(), route, params);
private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException {
HttpURLConnection connection = Requester.getConnectionFromRoute(SettingsEnum.SB_API_URL.getString(), route, params);
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
return connection;
}
private static JSONObject getJSONObject(Route route, String... params) throws Exception {
return Requester.parseJSONObjectAndDisconnect(getConnectionFromRoute(route, params));
private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException {
return Requester.parseJSONObject(getConnectionFromRoute(route, params));
}
}

View File

@ -9,11 +9,11 @@ class SBRoutes {
static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}");
static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}");
static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}");
static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]");
static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"viewCount\",\"minutesSaved\"]");
static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}");
static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}&videoDuration={duration}");
static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}");
static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}");
static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?userID={user_id}&videoID={video_id}&category={category}&startTime={start_time}&endTime={end_time}&videoDuration={duration}");
static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}");
static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}");
private SBRoutes() {
}

View File

@ -0,0 +1,124 @@
package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class CreateSegmentButtonController {
private static WeakReference<ImageView> buttonReference = new WeakReference<>(null);
private static Animation fadeIn;
private static Animation fadeOut;
private static boolean isShowing;
/**
* injection point
*/
public static void initialize(Object viewStub) {
try {
LogHelper.printDebug(() -> "initializing new segment button");
RelativeLayout youtubeControlsLayout = (RelativeLayout) viewStub;
String buttonIdentifier = "sb_sponsorblock_button";
ImageView imageView = youtubeControlsLayout.findViewById(getResourceIdentifier(buttonIdentifier, "id"));
if (imageView == null) {
LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonIdentifier + "\"");
return;
}
imageView.setOnClickListener(v -> {
LogHelper.printDebug(() -> "New segment button clicked");
SponsorBlockViewController.toggleNewSegmentLayoutVisibility();
});
buttonReference = new WeakReference<>(imageView);
// Animations
if (fadeIn == null) {
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
}
isShowing = true;
changeVisibilityImmediate(false);
} catch (Exception ex) {
LogHelper.printException(() -> "initialize failure", ex);
}
}
public static void changeVisibilityImmediate(boolean visible) {
changeVisibility(visible, true);
}
/**
* injection point
*/
public static void changeVisibilityNegatedImmediate(boolean visible) {
changeVisibility(!visible, true);
}
/**
* injection point
*/
public static void changeVisibility(boolean visible) {
changeVisibility(visible, false);
}
public static void changeVisibility(boolean visible, boolean immediate) {
try {
if (isShowing == visible) return;
isShowing = visible;
ImageView iView = buttonReference.get();
if (iView == null) return;
if (visible) {
iView.clearAnimation();
if (!shouldBeShown()) {
return;
}
if (!immediate) {
iView.startAnimation(fadeIn);
}
iView.setVisibility(View.VISIBLE);
return;
}
if (iView.getVisibility() == View.VISIBLE) {
iView.clearAnimation();
if (!immediate) {
iView.startAnimation(fadeOut);
}
iView.setVisibility(View.GONE);
}
} catch (Exception ex) {
LogHelper.printException(() -> "changeVisibility failure", ex);
}
}
private static boolean shouldBeShown() {
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_CREATE_NEW_SEGMENT_ENABLED.getBoolean()
&& !VideoInformation.isAtEndOfVideo();
}
public static void hide() {
if (!isShowing) {
return;
}
ReVancedUtils.verifyOnMainThread();
View v = buttonReference.get();
if (v == null) {
return;
}
v.setVisibility(View.GONE);
isShowing = false;
}
}

View File

@ -0,0 +1,124 @@
package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
import app.revanced.integrations.utils.LogHelper;
public class NewSegmentLayout extends FrameLayout {
private final int rippleEffectId;
final int defaultBottomMargin;
final int ctaBottomMargin;
public NewSegmentLayout(Context context) {
this(context, null);
}
public NewSegmentLayout(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr) {
this(context, attributeSet, defStyleAttr, 0);
}
public NewSegmentLayout(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
super(context, attributeSet, defStyleAttr, defStyleRes);
LayoutInflater.from(context).inflate(getResourceIdentifier(context, "new_segment", "layout"), this, true);
TypedValue rippleEffect = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true);
rippleEffectId = rippleEffect.resourceId;
// LinearLayout newSegmentContainer = findViewById(getResourceIdentifier(context, "sb_new_segment_container", "id"));
ImageButton rewindButton = findViewById(getResourceIdentifier(context, "sb_new_segment_rewind", "id"));
if (rewindButton == null) {
LogHelper.printException(() -> "Could not find rewindButton");
} else {
setClickEffect(rewindButton);
rewindButton.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Rewind button clicked");
VideoInformation.seekToRelative(-SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
});
}
ImageButton forwardButton = findViewById(getResourceIdentifier(context, "sb_new_segment_forward", "id"));
if (forwardButton == null) {
LogHelper.printException(() -> "Could not find forwardButton");
} else {
setClickEffect(forwardButton);
forwardButton.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Forward button clicked");
VideoInformation.seekToRelative(SettingsEnum.SB_ADJUST_NEW_SEGMENT_STEP.getInt());
});
}
ImageButton adjustButton = findViewById(getResourceIdentifier(context, "sb_new_segment_adjust", "id"));
if (adjustButton == null) {
LogHelper.printException(() -> "Could not find adjustButton");
} else {
setClickEffect(adjustButton);
adjustButton.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Adjust button clicked");
SponsorBlockUtils.onMarkLocationClicked();
});
}
ImageButton compareButton = findViewById(getResourceIdentifier(context, "sb_new_segment_compare", "id"));
if (compareButton == null) {
LogHelper.printException(() -> "Could not find compareButton");
} else {
setClickEffect(compareButton);
compareButton.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Compare button clicked");
SponsorBlockUtils.onPreviewClicked();
});
}
ImageButton editButton = findViewById(getResourceIdentifier(context, "sb_new_segment_edit", "id"));
if (editButton == null) {
LogHelper.printException(() -> "Could not find editButton");
} else {
setClickEffect(editButton);
editButton.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Edit button clicked");
SponsorBlockUtils.onEditByHandClicked();
});
}
ImageButton publishButton = findViewById(getResourceIdentifier(context, "sb_new_segment_publish", "id"));
if (publishButton == null) {
LogHelper.printException(() -> "Could not find publishButton");
} else {
setClickEffect(publishButton);
publishButton.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Publish button clicked");
SponsorBlockUtils.onPublishClicked();
});
}
defaultBottomMargin = getResourceDimensionPixelSize("brand_interaction_default_bottom_margin");
ctaBottomMargin = getResourceDimensionPixelSize("brand_interaction_cta_bottom_margin");
}
private void setClickEffect(ImageButton btn) {
btn.setBackgroundResource(rippleEffectId);
RippleDrawable rippleDrawable = (RippleDrawable) btn.getBackground();
int[][] states = new int[][]{new int[]{android.R.attr.state_enabled}};
int[] colors = new int[]{0x33ffffff}; // sets the ripple color to white
ColorStateList colorStateList = new ColorStateList(states, colors);
rippleDrawable.setColor(colorStateList);
}
}

View File

@ -0,0 +1,103 @@
package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceColor;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimension;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceDimensionPixelSize;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import static app.revanced.integrations.utils.StringRef.str;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.utils.LogHelper;
public class SkipSponsorButton extends FrameLayout {
private static final boolean highContrast = true;
private final LinearLayout skipSponsorBtnContainer;
private final TextView skipSponsorTextView;
private final CharSequence skipSponsorTextCompact;
private final Paint background;
private final Paint border;
final int defaultBottomMargin;
final int ctaBottomMargin;
public SkipSponsorButton(Context context) {
this(context, null);
}
public SkipSponsorButton(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) {
this(context, attributeSet, defStyleAttr, 0);
}
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
super(context, attributeSet, defStyleAttr, defStyleRes);
LayoutInflater.from(context).inflate(getResourceIdentifier(context, "skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height
skipSponsorBtnContainer = Objects.requireNonNull((LinearLayout) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container
background = new Paint();
background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color);
background.setStyle(Paint.Style.FILL);
border = new Paint();
border.setColor(getResourceColor("skip_ad_button_border_color")); // color:skip_ad_button_border_color);
border.setStrokeWidth(getResourceDimension("ad_skip_ad_button_border_width")); // dimen:ad_skip_ad_button_border_width);
border.setStyle(Paint.Style.STROKE);
skipSponsorTextView = Objects.requireNonNull((TextView) findViewById(getResourceIdentifier(context, "sb_skip_sponsor_button_text", "id"))); // id:skip_ad_button_text;
defaultBottomMargin = getResourceDimensionPixelSize("skip_button_default_bottom_margin"); // dimen:skip_button_default_bottom_margin
ctaBottomMargin = getResourceDimensionPixelSize("skip_button_cta_bottom_margin"); // dimen:skip_button_cta_bottom_margin
skipSponsorTextCompact = str("sb_skip_button_compact"); // string:skip_ads "Skip ads"
skipSponsorBtnContainer.setOnClickListener(v -> {
LogHelper.printDebug(() -> "Skip button clicked");
SegmentPlaybackController.onSkipSponsorClicked();
});
}
@Override // android.view.ViewGroup
protected final void dispatchDraw(Canvas canvas) {
final int left = skipSponsorBtnContainer.getLeft();
final int top = skipSponsorBtnContainer.getTop();
final int leftPlusWidth = (left + skipSponsorBtnContainer.getWidth());
final int topPlusHeight = (top + skipSponsorBtnContainer.getHeight());
canvas.drawRect(left, top, leftPlusWidth, topPlusHeight, background);
if (!highContrast) {
canvas.drawLines(new float[]{
leftPlusWidth, top, left, top,
left, top, left, topPlusHeight,
left, topPlusHeight, leftPlusWidth, topPlusHeight},
border);
}
super.dispatchDraw(canvas);
}
/**
* @return true, if this button state was changed
*/
public boolean updateSkipButtonText(SponsorSegment segment) {
CharSequence newText = SettingsEnum.SB_USE_COMPACT_SKIPBUTTON.getBoolean()
? skipSponsorTextCompact
: segment.getSkipButtonText();
if (newText.equals(skipSponsorTextView.getText())) {
return false;
}
skipSponsorTextView.setText(newText);
return true;
}
}

View File

@ -0,0 +1,228 @@
package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.sponsorblock.objects.SponsorSegment;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class SponsorBlockViewController {
private static WeakReference<RelativeLayout> inlineSponsorOverlayRef = new WeakReference<>(null);
private static WeakReference<ViewGroup> youtubeOverlaysLayoutRef = new WeakReference<>(null);
private static WeakReference<SkipSponsorButton> skipSponsorButtonRef = new WeakReference<>(null);
private static WeakReference<NewSegmentLayout> newSegmentLayoutRef = new WeakReference<>(null);
private static boolean canShowViewElements = true;
@Nullable
private static SponsorSegment skipSegment;
static {
PlayerType.getOnChange().addObserver((PlayerType type) -> {
playerTypeChanged(type);
return null;
});
}
public static Context getOverLaysViewGroupContext() {
ViewGroup group = youtubeOverlaysLayoutRef.get();
if (group == null) {
return null;
}
return group.getContext();
}
/**
* Injection point.
*/
public static void initialize(Object obj) {
try {
LogHelper.printDebug(() -> "initializing");
RelativeLayout layout = new RelativeLayout(ReVancedUtils.getContext());
layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT));
LayoutInflater.from(ReVancedUtils.getContext()).inflate(getResourceIdentifier("inline_sponsor_overlay", "layout"), layout);
inlineSponsorOverlayRef = new WeakReference<>(layout);
ViewGroup viewGroup = (ViewGroup) obj;
viewGroup.addView(layout, viewGroup.getChildCount() - 2);
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
skipSponsorButtonRef = new WeakReference<>(
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_skip_sponsor_button", "id"))));
newSegmentLayoutRef = new WeakReference<>(
Objects.requireNonNull(layout.findViewById(getResourceIdentifier("sb_new_segment_view", "id"))));
} catch (Exception ex) {
LogHelper.printException(() -> "initialize failure", ex);
}
}
public static void showSkipButton(@NonNull SponsorSegment info) {
skipSegment = Objects.requireNonNull(info);
updateSkipButton();
}
public static void hideSkipButton() {
skipSegment = null;
updateSkipButton();
}
private static void updateSkipButton() {
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
if (skipSponsorButton == null) {
return;
}
if (skipSegment == null) {
setSkipSponsorButtonVisibility(false);
} else {
final boolean layoutNeedsUpdating = skipSponsorButton.updateSkipButtonText(skipSegment);
if (layoutNeedsUpdating) {
bringLayoutToFront();
}
setSkipSponsorButtonVisibility(true);
}
}
public static void showNewSegmentLayout() {
setNewSegmentLayoutVisibility(true);
}
public static void hideNewSegmentLayout() {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
return;
}
setNewSegmentLayoutVisibility(false);
}
public static void toggleNewSegmentLayoutVisibility() {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "toggleNewSegmentLayoutVisibility failure");
return;
}
setNewSegmentLayoutVisibility(newSegmentLayout.getVisibility() == View.VISIBLE ? false : true);
}
private static void playerTypeChanged(PlayerType playerType) {
try {
final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN;
canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED);
setSkipButtonMargins(isWatchFullScreen);
setNewSegmentLayoutMargins(isWatchFullScreen);
updateSkipButton();
} catch (Exception ex) {
LogHelper.printException(() -> "Player type changed error", ex);
}
}
private static void setSkipButtonMargins(boolean fullScreen) {
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
if (skipSponsorButton == null) {
LogHelper.printException(() -> "setSkipButtonMargins failure");
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) skipSponsorButton.getLayoutParams();
if (params == null) {
LogHelper.printException(() -> "setSkipButtonMargins failure");
return;
}
params.bottomMargin = fullScreen ? skipSponsorButton.ctaBottomMargin : skipSponsorButton.defaultBottomMargin;
skipSponsorButton.setLayoutParams(params);
}
private static void setSkipSponsorButtonVisibility(boolean visible) {
SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get();
if (skipSponsorButton == null) {
LogHelper.printException(() -> "setSkipSponsorButtonVisibility failure");
return;
}
visible &= canShowViewElements;
final int desiredVisibility = visible ? View.VISIBLE : View.GONE;
if (skipSponsorButton.getVisibility() != desiredVisibility) {
skipSponsorButton.setVisibility(desiredVisibility);
if (visible) {
bringLayoutToFront();
}
}
}
private static void setNewSegmentLayoutMargins(boolean fullScreen) {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (button is null)");
return;
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) newSegmentLayout.getLayoutParams();
if (params == null) {
LogHelper.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)");
return;
}
params.bottomMargin = fullScreen ? newSegmentLayout.ctaBottomMargin : newSegmentLayout.defaultBottomMargin;
newSegmentLayout.setLayoutParams(params);
}
private static void setNewSegmentLayoutVisibility(boolean visible) {
NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get();
if (newSegmentLayout == null) {
LogHelper.printException(() -> "setNewSegmentLayoutVisibility failure");
return;
}
visible &= canShowViewElements;
final int desiredVisibility = visible ? View.VISIBLE : View.GONE;
if (newSegmentLayout.getVisibility() != desiredVisibility) {
newSegmentLayout.setVisibility(desiredVisibility);
if (visible) {
bringLayoutToFront();
}
}
}
private static void bringLayoutToFront() {
RelativeLayout layout = inlineSponsorOverlayRef.get();
if (layout != null) {
// needed to keep skip button overtop end screen cards
layout.bringToFront();
layout.requestLayout();
layout.invalidate();
}
}
/**
* Injection point.
*/
public static void endOfVideoReached() {
try {
LogHelper.printDebug(() -> "endOfVideoReached");
// the buttons automatically set themselves to visible when appropriate,
// but if buttons are showing when the end of the video is reached then they need
// to be forcefully hidden
if (!SettingsEnum.PREFERRED_AUTO_REPEAT.getBoolean()) {
CreateSegmentButtonController.hide();
VotingButtonController.hide();
}
} catch (Exception ex) {
LogHelper.printException(() -> "endOfVideoReached failure", ex);
}
}
}

View File

@ -0,0 +1,125 @@
package app.revanced.integrations.sponsorblock.ui;
import static app.revanced.integrations.utils.ReVancedUtils.getResourceIdentifier;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import java.lang.ref.WeakReference;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.SegmentPlaybackController;
import app.revanced.integrations.sponsorblock.SponsorBlockUtils;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public class VotingButtonController {
private static WeakReference<ImageView> buttonReference = new WeakReference<>(null);
private static Animation fadeIn;
private static Animation fadeOut;
private static boolean isShowing;
/**
* injection point
*/
public static void initialize(Object viewStub) {
try {
LogHelper.printDebug(() -> "initializing voting button");
RelativeLayout controlsLayout = (RelativeLayout) viewStub;
String buttonResourceName = "sb_voting_button";
ImageView imageView = controlsLayout.findViewById(getResourceIdentifier(buttonResourceName, "id"));
if (imageView == null) {
LogHelper.printException(() -> "Couldn't find imageView with \"" + buttonResourceName + "\"");
return;
}
imageView.setOnClickListener(v -> {
SponsorBlockUtils.onVotingClicked(v.getContext());
});
buttonReference = new WeakReference<>(imageView);
// Animations
if (fadeIn == null) {
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
}
isShowing = true;
changeVisibilityImmediate(false);
} catch (Exception ex) {
LogHelper.printException(() -> "Unable to set RelativeLayout", ex);
}
}
public static void changeVisibilityImmediate(boolean visible) {
changeVisibility(visible, true);
}
/**
* injection point
*/
public static void changeVisibilityNegatedImmediate(boolean visible) {
changeVisibility(!visible, true);
}
/**
* injection point
*/
public static void changeVisibility(boolean visible) {
changeVisibility(visible, false);
}
public static void changeVisibility(boolean visible, boolean immediate) {
try {
if (isShowing == visible) return;
isShowing = visible;
ImageView iView = buttonReference.get();
if (iView == null) return;
if (visible) {
iView.clearAnimation();
if (!shouldBeShown()) {
return;
}
if (!immediate) {
iView.startAnimation(fadeIn);
}
iView.setVisibility(View.VISIBLE);
return;
}
if (iView.getVisibility() == View.VISIBLE) {
iView.clearAnimation();
if (!immediate) {
iView.startAnimation(fadeOut);
}
iView.setVisibility(View.GONE);
}
} catch (Exception ex) {
LogHelper.printException(() -> "changeVisibility failure", ex);
}
}
private static boolean shouldBeShown() {
return SettingsEnum.SB_ENABLED.getBoolean() && SettingsEnum.SB_VOTING_ENABLED.getBoolean()
&& SegmentPlaybackController.currentVideoHasSegments() && !VideoInformation.isAtEndOfVideo();
}
public static void hide() {
if (!isShowing) {
return;
}
ReVancedUtils.verifyOnMainThread();
View v = buttonReference.get();
if (v == null) {
LogHelper.printDebug(() -> "Cannot hide voting button (value is null)");
return;
}
v.setVisibility(View.GONE);
isShowing = false;
}
}

View File

@ -16,8 +16,8 @@ import app.revanced.integrations.swipecontrols.controller.gesture.PressToSwipeCo
import app.revanced.integrations.swipecontrols.controller.gesture.core.GestureController
import app.revanced.integrations.swipecontrols.misc.Rectangle
import app.revanced.integrations.swipecontrols.views.SwipeControlsOverlayLayout
import app.revanced.integrations.utils.LogHelper.printDebug
import app.revanced.integrations.utils.LogHelper.printException
import app.revanced.integrations.utils.LogHelper.printInfo
import java.lang.ref.WeakReference
/**
@ -121,7 +121,7 @@ class SwipeControlsHostActivity : Activity() {
*/
private fun initialize() {
// create controllers
printInfo { "initializing swipe controls controllers" }
printDebug { "initializing swipe controls controllers" }
config = SwipeControlsConfigurationProvider(this)
keys = VolumeKeysController(this)
audio = createAudioController()
@ -157,7 +157,7 @@ class SwipeControlsHostActivity : Activity() {
* (re) attaches swipe overlays
*/
private fun reAttachOverlays() {
printInfo { "attaching swipe controls overlay" }
printDebug{ "attaching swipe controls overlay" }
contentRoot.removeView(overlay)
contentRoot.addView(overlay)
}

View File

@ -56,7 +56,7 @@ class SwipeZonesController(
/**
* id for R.id.player_view
*/
private val playerViewId = ReVancedUtils.getResourceIdByName(host, "id", "player_view")
private val playerViewId = ReVancedUtils.getResourceIdentifier(host, "player_view", "id")
/**
* current bounding rectangle of the player

View File

@ -39,7 +39,7 @@ class SwipeControlsOverlayLayout(
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
return resources.getDrawable(
ReVancedUtils.getResourceIdByName(context, "drawable", name),
ReVancedUtils.getResourceIdentifier(context, name, "drawable"),
context.theme
).apply {
setTint(config.overlayForegroundColor)

View File

@ -7,10 +7,18 @@ class Event<T> {
private val eventListeners = mutableSetOf<(T) -> Unit>()
operator fun plusAssign(observer: (T) -> Unit) {
addObserver(observer)
}
fun addObserver(observer: (T) -> Unit) {
eventListeners.add(observer)
}
operator fun minusAssign(observer: (T) -> Unit) {
removeObserver(observer)
}
fun removeObserver(observer: (T) -> Unit) {
eventListeners.remove(observer)
}

View File

@ -1,8 +1,6 @@
package app.revanced.integrations.utils;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -131,12 +129,7 @@ public class LogHelper {
String toastMessageToDisplay = (userToastMessage != null)
? userToastMessage
: outerClassSimpleName + ": " + messageString;
ReVancedUtils.runOnMainThread(() -> {
Context context = ReVancedUtils.getContext();
if (context != null) {
Toast.makeText(context, toastMessageToDisplay, Toast.LENGTH_LONG).show();
}
});
ReVancedUtils.showToastLong(toastMessageToDisplay);
}
}

View File

@ -3,134 +3,119 @@ package app.revanced.integrations.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.os.Handler;
import android.os.Looper;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.Bidi;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import app.revanced.integrations.sponsorblock.player.PlayerType;
public class ReVancedUtils {
private static PlayerType env;
private static boolean newVideo = false;
@SuppressLint("StaticFieldLeak")
public static Context context;
private ReVancedUtils() {
} // utility class
/**
* Maximum number of background threads run concurrently
*/
private static final int SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS = 20;
/**
* General purpose pool for network calls and other background tasks.
* All tasks run at max thread priority.
*/
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
2, // minimum 2 threads always ready to be used
2, // 2 threads always ready to go
Integer.MAX_VALUE,
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
new SynchronousQueue<>(),
r -> { // ThreadFactory
Thread t = new Thread(r);
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
return t;
}
});
private static void checkIfPoolHasReachedLimit() {
if (backgroundThreadPool.getActiveCount() >= SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS) {
// Something is wrong. Background threads are piling up and not completing as expected,
// or some ReVanced code is submitting an unexpected number of background tasks.
LogHelper.printException(() -> "Reached maximum background thread count of "
+ SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS + " threads");
}
}
public static void runOnBackgroundThread(Runnable task) {
public static void runOnBackgroundThread(@NonNull Runnable task) {
backgroundThreadPool.execute(task);
checkIfPoolHasReachedLimit();
}
public static <T> Future<T> submitOnBackgroundThread(Callable<T> call) {
Future<T> future = backgroundThreadPool.submit(call);
checkIfPoolHasReachedLimit();
return future;
@NonNull
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
return backgroundThreadPool.submit(call);
}
public static boolean containsAny(final String value, final String... targets) {
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
for (String string : targets)
if (!string.isEmpty() && value.contains(string)) return true;
return false;
}
public static void setNewVideo(boolean started) {
LogHelper.printDebug(() -> "New video started: " + started);
newVideo = started;
/**
* @return zero, if the resource is not found
*/
@SuppressLint("DiscouragedApi")
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
}
public static boolean isNewVideoStarted() {
return newVideo;
/**
* @return zero, if the resource is not found
*/
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
}
public static Integer getResourceIdByName(Context context, String type, String name) {
try {
Resources res = context.getResources();
return res.getIdentifier(name, type, context.getPackageName());
} catch (Throwable exception) {
LogHelper.printException(() -> "Resource not found.", exception);
return null;
}
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
}
public static void setPlayerType(PlayerType type) {
env = type;
@NonNull
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
}
public static PlayerType getPlayerType() {
return env;
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
}
public static int getIdentifier(String name, String defType) {
Context context = getContext();
return context.getResources().getIdentifier(name, defType, context.getPackageName());
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
public static Context getContext() {
if (context != null) {
return context;
} else {
}
LogHelper.printException(() -> "Context is null, returning null!");
return null;
}
}
public static void setClipboard(String text) {
public static void setClipboard(@NonNull String text) {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
clipboard.setPrimaryClip(clip);
}
public static boolean isTablet(Context context) {
public static boolean isTablet() {
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
@Nullable
private static Boolean isRightToLeftTextLayout;
/**
* If the device language uses right to left text layout (hebrew, arabic, etc)
@ -144,16 +129,46 @@ public class ReVancedUtils {
}
/**
* Automatically logs any exceptions the runnable throws
* Safe to call from any thread
*/
public static void runOnMainThread(Runnable runnable) {
public static void showToastShort(@NonNull String messageToToast) {
showToast(messageToToast, Toast.LENGTH_SHORT);
}
/**
* Safe to call from any thread
*/
public static void showToastLong(@NonNull String messageToToast) {
showToast(messageToToast, Toast.LENGTH_LONG);
}
private static void showToast(@NonNull String messageToToast, int toastDuration) {
Objects.requireNonNull(messageToToast);
runOnMainThreadNowOrLater(() -> {
// cannot use getContext(), otherwise if context is null it will cause infinite recursion of error logging
if (context == null) {
LogHelper.printDebug(() -> "Cannot show toast (context is null)");
} else {
LogHelper.printDebug(() -> "Showing toast: " + messageToToast);
Toast.makeText(context, messageToToast, toastDuration).show();
}
}
);
}
/**
* Automatically logs any exceptions the runnable throws.
*
* @see #runOnMainThreadNowOrLater(Runnable)
*/
public static void runOnMainThread(@NonNull Runnable runnable) {
runOnMainThreadDelayed(runnable, 0);
}
/**
* Automatically logs any exceptions the runnable throws
*/
public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) {
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
Runnable loggingRunnable = () -> {
try {
runnable.run();
@ -164,10 +179,22 @@ public class ReVancedUtils {
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
}
/**
* If called from the main thread, the code is run immediately.<p>
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
*/
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
if (isCurrentlyOnMainThread()) {
runnable.run();
} else {
runOnMainThread(runnable);
}
}
/**
* @return if the calling thread is on the main thread
*/
public static boolean currentlyIsOnMainThread() {
public static boolean isCurrentlyOnMainThread() {
return Looper.getMainLooper().isCurrentThread();
}
@ -175,7 +202,7 @@ public class ReVancedUtils {
* @throws IllegalStateException if the calling thread is _off_ the main thread
*/
public static void verifyOnMainThread() throws IllegalStateException {
if (!currentlyIsOnMainThread()) {
if (!isCurrentlyOnMainThread()) {
throw new IllegalStateException("Must call _on_ the main thread");
}
}
@ -184,8 +211,37 @@ public class ReVancedUtils {
* @throws IllegalStateException if the calling thread is _on_ the main thread
*/
public static void verifyOffMainThread() throws IllegalStateException {
if (currentlyIsOnMainThread()) {
if (isCurrentlyOnMainThread()) {
throw new IllegalStateException("Must call _off_ the main thread");
}
}
public static boolean isNetworkConnected() {
NetworkType networkType = getNetworkType();
return networkType == NetworkType.MOBILE
|| networkType == NetworkType.OTHER;
}
@SuppressLint("MissingPermission") // permission already included in YouTube
public static NetworkType getNetworkType() {
Context networkContext = getContext();
if (networkContext == null) {
return NetworkType.NONE;
}
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
var networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected()) {
return NetworkType.NONE;
}
var type = networkInfo.getType();
return (type == ConnectivityManager.TYPE_MOBILE)
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
}
public enum NetworkType {
NONE,
MOBILE,
OTHER,
}
}

View File

@ -36,7 +36,7 @@ public class SharedPrefHelper {
// region Hack, unknown why required
public static Long getLong(SharedPrefNames prefName, String key, Long _default) {
public static Long getLong(SharedPrefNames prefName, String key, long _default) {
SharedPreferences sharedPreferences = getPreferences(prefName);
try {
return Long.valueOf(sharedPreferences.getString(key, _default + ""));
@ -45,7 +45,7 @@ public class SharedPrefHelper {
}
}
public static Float getFloat(SharedPrefNames prefName, String key, Float _default) {
public static Float getFloat(SharedPrefNames prefName, String key, float _default) {
SharedPreferences sharedPreferences = getPreferences(prefName);
try {
return Float.valueOf(sharedPreferences.getString(key, _default + ""));
@ -54,7 +54,7 @@ public class SharedPrefHelper {
}
}
public static Integer getInt(SharedPrefNames prefName, String key, Integer _default) {
public static Integer getInt(SharedPrefNames prefName, String key, int _default) {
SharedPreferences sharedPreferences = getPreferences(prefName);
try {
return Integer.valueOf(sharedPreferences.getString(key, _default + ""));

View File

@ -1,4 +1,4 @@
package app.revanced.integrations.sponsorblock;
package app.revanced.integrations.utils;
import android.content.Context;
import android.content.res.Resources;
@ -9,26 +9,22 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
// should probably move this class into utils package
public class StringRef {
private static Resources resources;
private static String packageName;
// must use a thread safe map, as this class is used both on and off the main thread
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap());
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap<>());
/**
* Gets strings reference from shared collection or creates if not exists yet,
* this method should be called if you want to get StringRef
* Returns a cached instance.
* Should be used if the same String could be loaded more than once.
*
* @param id string resource name/id
* @return String reference that'll resolve to excepted string, may be from cache
* @see #sf(String)
*/
@NonNull
public static StringRef sf(@NonNull String id) {
public static StringRef sfc(@NonNull String id) {
StringRef ref = strings.get(id);
if (ref == null) {
ref = new StringRef(id);
@ -38,18 +34,30 @@ public class StringRef {
}
/**
* Gets string value by string id, shorthand for <code>sf(id).toString()</code>
* Creates a new instance, but does not cache the value.
* Should be used for Strings that are loaded exactly once.
*
* @param id string resource name/id
* @see #sfc(String)
*/
@NonNull
public static StringRef sf(@NonNull String id) {
return new StringRef(id);
}
/**
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code>
*
* @param id string resource name/id
* @return String value from string.xml
*/
@NonNull
public static String str(@NonNull String id) {
return sf(id).toString();
return sfc(id).toString();
}
/**
* Gets string value by string id, shorthand for <code>sf(id).toString()</code> and formats the string
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code> and formats the string
* with given args.
*
* @param id string resource name/id
@ -61,7 +69,6 @@ public class StringRef {
return String.format(str(id), args);
}
/**
* Creates a StringRef object that'll not change it's value
*

View File

@ -4,14 +4,19 @@ public class ThemeHelper {
private static int themeValue;
public static void setTheme(int value) {
if (themeValue != value) {
themeValue = value;
LogHelper.printDebug(() -> "Theme value: " + themeValue);
}
}
public static void setTheme(Object value) {
themeValue = ((Enum) value).ordinal();
final int newOrdinalValue = ((Enum) value).ordinal();
if (themeValue != newOrdinalValue) {
themeValue = newOrdinalValue;
LogHelper.printDebug(() -> "Theme value: " + themeValue);
}
}
public static boolean isDarkTheme() {
return themeValue == 1;

View File

@ -3,7 +3,6 @@ package app.revanced.integrations.videoplayer;
import android.support.constraint.ConstraintLayout;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import java.lang.ref.WeakReference;
@ -26,22 +25,19 @@ public abstract class BottomControlButton {
constraintLayout = (ConstraintLayout) obj;
isButtonEnabled = isEnabled;
ImageView imageView = constraintLayout.findViewById(ReVancedUtils.getIdentifier(viewId, "id"));
ImageView imageView = constraintLayout.findViewById(ReVancedUtils.getResourceIdentifier(viewId, "id"));
if (imageView == null) {
LogHelper.printDebug(() -> "Couldn't find ImageView with id: " + viewId);
LogHelper.printException(() -> "Couldn't find ImageView with id: " + viewId);
return;
}
imageView.setOnClickListener(onClickListener);
button = new WeakReference<>(imageView);
fadeIn = getAnimation("fade_in");
fadeOut = getAnimation("fade_out");
int fadeDurationFast = getInteger("fade_duration_fast");
int fadeDurationScheduled = getInteger("fade_duration_scheduled");
fadeIn.setDuration(fadeDurationFast);
fadeOut.setDuration(fadeDurationScheduled);
fadeIn = ReVancedUtils.getResourceAnimation("fade_in");
fadeOut = ReVancedUtils.getResourceAnimation("fade_out");
fadeIn.setDuration(ReVancedUtils.getResourceInteger("fade_duration_fast"));
fadeOut.setDuration(ReVancedUtils.getResourceInteger("fade_duration_scheduled"));
isShowing = true;
setVisibility(false);
} catch (Exception e) {
@ -69,11 +65,4 @@ public abstract class BottomControlButton {
imageView.setVisibility(View.GONE);
}
}
private static int getInteger(String str) {
return ReVancedUtils.getContext().getResources().getInteger(ReVancedUtils.getIdentifier(str, "integer"));
}
private static Animation getAnimation(String str) {
return AnimationUtils.loadAnimation(ReVancedUtils.getContext(), ReVancedUtils.getIdentifier(str, "anim"));
}
}

View File

@ -3,12 +3,12 @@ package app.revanced.integrations.videoplayer;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.view.View;
import android.widget.Toast;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.sponsorblock.StringRef;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.StringRef;
public class DownloadButton extends BottomControlButton {
public static DownloadButton instance;
@ -46,7 +46,7 @@ public class DownloadButton extends BottomControlButton {
// If the package is not installed, show the toast
if (!packageEnabled) {
Toast.makeText(context, downloaderPackageName + " " + StringRef.str("downloader_not_installed_warning"), Toast.LENGTH_LONG).show();
ReVancedUtils.showToastLong(downloaderPackageName + " " + StringRef.str("downloader_not_installed_warning"));
return;
}