mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-06-12 05:07:45 +02:00
chore: Separate extensions by app (#3905)
This commit is contained in:
@ -1,22 +1,3 @@
|
||||
extension {
|
||||
name = "extensions/shared.rve"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.appcompat)
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
compileOnly(libs.retrofit)
|
||||
|
||||
compileOnly(project(":extensions:shared:stub"))
|
||||
implementation(project(":extensions:shared:library"))
|
||||
}
|
||||
|
21
extensions/shared/library/build.gradle.kts
Normal file
21
extensions/shared/library/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
}
|
@ -19,7 +19,7 @@ import java.util.Collection;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
abstract class Check {
|
||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||
@ -46,11 +46,11 @@ abstract class Check {
|
||||
/**
|
||||
* For debugging and development only.
|
||||
* Forces all checks to be performed and the check failed dialog to be shown.
|
||||
* Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
|
||||
* Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
|
||||
* set to -1.
|
||||
*/
|
||||
static boolean debugAlwaysShowWarning() {
|
||||
final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
|
||||
final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
|
||||
if (alwaysShowWarning) {
|
||||
Logger.printInfo(() -> "Debug forcing environment check warning to show");
|
||||
}
|
||||
@ -59,14 +59,14 @@ abstract class Check {
|
||||
}
|
||||
|
||||
static boolean shouldRun() {
|
||||
return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
|
||||
return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
|
||||
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
|
||||
}
|
||||
|
||||
static void disableForever() {
|
||||
Logger.printInfo(() -> "Environment checks disabled forever");
|
||||
|
||||
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@ -107,8 +107,8 @@ abstract class Check {
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||
final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.extension.youtube.requests;
|
||||
package app.revanced.extension.shared.requests;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import org.json.JSONArray;
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.extension.youtube.requests;
|
||||
package app.revanced.extension.shared.requests;
|
||||
|
||||
public class Route {
|
||||
private final String route;
|
@ -14,4 +14,6 @@ public class BaseSettings {
|
||||
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
|
||||
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
|
||||
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
|
||||
|
||||
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
|
||||
}
|
@ -7,7 +7,6 @@ import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringRef;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@ -62,6 +61,30 @@ public abstract class Setting<T> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for importing/exporting settings.
|
||||
*/
|
||||
public interface ImportExportCallback {
|
||||
/**
|
||||
* Called after all settings have been imported.
|
||||
*/
|
||||
void settingsImported(@Nullable Context context);
|
||||
|
||||
/**
|
||||
* Called after all settings have been exported.
|
||||
*/
|
||||
void settingsExported(@Nullable Context context);
|
||||
}
|
||||
|
||||
private static final List<ImportExportCallback> importExportCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||
*/
|
||||
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
|
||||
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* All settings that were instantiated.
|
||||
* When a new setting is created, it is automatically added to this list.
|
||||
@ -365,7 +388,10 @@ public abstract class Setting<T> {
|
||||
setting.writeToJSON(json, importExportKey);
|
||||
}
|
||||
}
|
||||
SponsorBlockSettings.showExportWarningIfNeeded(alertDialogContext);
|
||||
|
||||
for (ImportExportCallback callback : importExportCallbacks) {
|
||||
callback.settingsExported(alertDialogContext);
|
||||
}
|
||||
|
||||
if (json.length() == 0) {
|
||||
return "";
|
||||
@ -385,7 +411,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* @return if any settings that require a reboot were changed.
|
||||
*/
|
||||
public static boolean importFromJSON(@NonNull String settingsJsonString) {
|
||||
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
|
||||
try {
|
||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||
@ -411,12 +437,9 @@ public abstract class Setting<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// SB Enum categories are saved using StringSettings.
|
||||
// Which means they need to reload again if changed by other code (such as here).
|
||||
// This call could be removed by creating a custom Setting class that manages the
|
||||
// "String <-> Enum" logic or by adding an event hook of when settings are imported.
|
||||
// But for now this is simple and works.
|
||||
SponsorBlockSettings.updateFromImportedSettings();
|
||||
for (ImportExportCallback callback : importExportCallbacks) {
|
||||
callback.settingsExported(alertDialogContext);
|
||||
}
|
||||
|
||||
Utils.showToastLong(numberOfSettingsImported == 0
|
||||
? str("revanced_settings_import_reset")
|
@ -85,7 +85,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
return;
|
||||
}
|
||||
AbstractPreferenceFragment.settingImportInProgress = true;
|
||||
final boolean rebootNeeded = Setting.importFromJSON(replacementSettings);
|
||||
|
||||
final boolean rebootNeeded = Setting.importFromJSON(Utils.getContext(), replacementSettings);
|
||||
if (rebootNeeded) {
|
||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.youtube.requests.Route.Method.GET;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
@ -34,8 +34,8 @@ import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.requests.Requester;
|
||||
import app.revanced.extension.youtube.requests.Route;
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
|
||||
/**
|
||||
* Opens a dialog showing official links.
|
9
extensions/shared/proguard-rules.pro
vendored
9
extensions/shared/proguard-rules.pro
vendored
@ -1,9 +0,0 @@
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
-keepattributes *
|
||||
-keep class app.revanced.** {
|
||||
*;
|
||||
}
|
||||
-keep class com.google.** {
|
||||
*;
|
||||
}
|
@ -1,4 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
</manifest>
|
||||
<manifest/>
|
||||
|
@ -1,24 +0,0 @@
|
||||
package app.revanced.extension.boostforreddit;
|
||||
|
||||
import com.rubenmayayo.reddit.ui.activities.WebViewActivity;
|
||||
|
||||
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
||||
static {
|
||||
INSTANCE = new FixSLinksPatch();
|
||||
}
|
||||
|
||||
private FixSLinksPatch() {
|
||||
webViewActivityClass = WebViewActivity.class;
|
||||
}
|
||||
|
||||
public static boolean patchResolveSLink(String link) {
|
||||
return INSTANCE.resolveSLink(link);
|
||||
}
|
||||
|
||||
public static void patchSetAccessToken(String accessToken) {
|
||||
INSTANCE.setAccessToken(accessToken);
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package app.revanced.extension.reddit.patches;
|
||||
|
||||
import com.reddit.domain.model.ILink;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class FilterPromotedLinksPatch {
|
||||
/**
|
||||
* Filters list from promoted links.
|
||||
**/
|
||||
public static List<?> filterChildren(final Iterable<?> links) {
|
||||
final List<Object> filteredList = new ArrayList<>();
|
||||
|
||||
for (Object item : links) {
|
||||
if (item instanceof ILink && ((ILink) item).getPromoted()) continue;
|
||||
|
||||
filteredList.add(item);
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package app.revanced.extension.syncforreddit;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.w3c.dom.Element;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class FixRedditVideoDownloadPatch {
|
||||
private static @Nullable Pair<Integer, String> getBestMpEntry(Element element) {
|
||||
var representations = element.getElementsByTagName("Representation");
|
||||
var entries = new ArrayList<Pair<Integer, String>>();
|
||||
|
||||
for (int i = 0; i < representations.getLength(); i++) {
|
||||
Element representation = (Element) representations.item(i);
|
||||
var bandwidthStr = representation.getAttribute("bandwidth");
|
||||
try {
|
||||
var bandwidth = Integer.parseInt(bandwidthStr);
|
||||
var baseUrl = representation.getElementsByTagName("BaseURL").item(0);
|
||||
if (baseUrl != null) {
|
||||
entries.add(new Pair<>(bandwidth, baseUrl.getTextContent()));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Collections.sort(entries, (e1, e2) -> e2.first - e1.first);
|
||||
return entries.get(0);
|
||||
}
|
||||
|
||||
private static String[] parse(byte[] data) throws ParserConfigurationException, IOException, SAXException {
|
||||
var adaptionSets = DocumentBuilderFactory
|
||||
.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(data))
|
||||
.getElementsByTagName("AdaptationSet");
|
||||
|
||||
String videoUrl = null;
|
||||
String audioUrl = null;
|
||||
|
||||
for (int i = 0; i < adaptionSets.getLength(); i++) {
|
||||
Element element = (Element) adaptionSets.item(i);
|
||||
var contentType = element.getAttribute("contentType");
|
||||
var bestEntry = getBestMpEntry(element);
|
||||
if (bestEntry == null) continue;
|
||||
|
||||
if (contentType.equalsIgnoreCase("video")) {
|
||||
videoUrl = bestEntry.second;
|
||||
} else if (contentType.equalsIgnoreCase("audio")) {
|
||||
audioUrl = bestEntry.second;
|
||||
}
|
||||
}
|
||||
|
||||
return new String[]{videoUrl, audioUrl};
|
||||
}
|
||||
|
||||
public static String[] getLinks(byte[] data) {
|
||||
try {
|
||||
return parse(data);
|
||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||
return new String[]{null, null};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package app.revanced.extension.syncforreddit;
|
||||
|
||||
import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity;
|
||||
|
||||
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class FixSLinksPatch extends BaseFixSLinksPatch {
|
||||
static {
|
||||
INSTANCE = new FixSLinksPatch();
|
||||
}
|
||||
|
||||
private FixSLinksPatch() {
|
||||
webViewActivityClass = WebViewActivity.class;
|
||||
}
|
||||
|
||||
public static boolean patchResolveSLink(String link) {
|
||||
return INSTANCE.resolveSLink(link);
|
||||
}
|
||||
|
||||
public static void patchSetAccessToken(String accessToken) {
|
||||
INSTANCE.setAccessToken(accessToken);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package app.revanced.extension.tiktok;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
public class Utils {
|
||||
|
||||
// Edit: This could be handled using a custom Setting<Long[]> class
|
||||
// that saves its value to preferences and JSON using the formatted String created here.
|
||||
public static long[] parseMinMax(StringSetting setting) {
|
||||
final String[] minMax = setting.get().split("-");
|
||||
if (minMax.length == 2) {
|
||||
try {
|
||||
final long min = Long.parseLong(minMax[0]);
|
||||
final long max = Long.parseLong(minMax[1]);
|
||||
|
||||
if (min <= max && min >= 0) return new long[]{min, max};
|
||||
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
setting.save("0-" + Long.MAX_VALUE);
|
||||
return new long[]{0L, Long.MAX_VALUE};
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package app.revanced.extension.tiktok.cleardisplay;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RememberClearDisplayPatch {
|
||||
public static boolean getClearDisplayState() {
|
||||
return Settings.CLEAR_DISPLAY.get();
|
||||
}
|
||||
public static void rememberClearDisplayState(boolean newState) {
|
||||
Settings.CLEAR_DISPLAY.save(newState);
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package app.revanced.extension.tiktok.download;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DownloadsPatch {
|
||||
public static String getDownloadPath() {
|
||||
return Settings.DOWNLOAD_PATH.get();
|
||||
}
|
||||
|
||||
public static boolean shouldRemoveWatermark() {
|
||||
return Settings.DOWNLOAD_WATERMARK.get();
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
public class AdsFilter implements IFilter {
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return Settings.REMOVE_ADS.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
return item.isAd() || item.isWithPromotionalMusic();
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
import com.ss.android.ugc.aweme.feed.model.FeedItemList;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public final class FeedItemsFilter {
|
||||
private static final List<IFilter> FILTERS = List.of(
|
||||
new AdsFilter(),
|
||||
new LiveFilter(),
|
||||
new StoryFilter(),
|
||||
new ImageVideoFilter(),
|
||||
new ViewCountFilter(),
|
||||
new LikeCountFilter()
|
||||
);
|
||||
|
||||
public static void filter(FeedItemList feedItemList) {
|
||||
Iterator<Aweme> feedItemListIterator = feedItemList.items.iterator();
|
||||
while (feedItemListIterator.hasNext()) {
|
||||
Aweme item = feedItemListIterator.next();
|
||||
if (item == null) continue;
|
||||
|
||||
for (IFilter filter : FILTERS) {
|
||||
boolean enabled = filter.getEnabled();
|
||||
if (enabled && filter.getFiltered(item)) {
|
||||
feedItemListIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
public interface IFilter {
|
||||
boolean getEnabled();
|
||||
|
||||
boolean getFiltered(Aweme item);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
public class ImageVideoFilter implements IFilter {
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return Settings.HIDE_IMAGE.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
return item.isImage() || item.isPhotoMode();
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
|
||||
|
||||
import static app.revanced.extension.tiktok.Utils.parseMinMax;
|
||||
|
||||
public final class LikeCountFilter implements IFilter {
|
||||
final long minLike;
|
||||
final long maxLike;
|
||||
|
||||
LikeCountFilter() {
|
||||
long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES);
|
||||
minLike = minMax[0];
|
||||
maxLike = minMax[1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
AwemeStatistics statistics = item.getStatistics();
|
||||
if (statistics == null) return false;
|
||||
|
||||
long likeCount = statistics.getDiggCount();
|
||||
return likeCount < minLike || likeCount > maxLike;
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
public class LiveFilter implements IFilter {
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return Settings.HIDE_LIVE.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
return item.isLive() || item.isLiveReplay();
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
|
||||
public class StoryFilter implements IFilter {
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return Settings.HIDE_STORY.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
return item.getIsTikTokStory();
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package app.revanced.extension.tiktok.feedfilter;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import com.ss.android.ugc.aweme.feed.model.Aweme;
|
||||
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
|
||||
|
||||
import static app.revanced.extension.tiktok.Utils.parseMinMax;
|
||||
|
||||
public class ViewCountFilter implements IFilter {
|
||||
final long minView;
|
||||
final long maxView;
|
||||
|
||||
ViewCountFilter() {
|
||||
long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS);
|
||||
minView = minMax[0];
|
||||
maxView = minMax[1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFiltered(Aweme item) {
|
||||
AwemeStatistics statistics = item.getStatistics();
|
||||
if (statistics == null) return false;
|
||||
|
||||
long playCount = statistics.getPlayCount();
|
||||
return playCount < minView || playCount > maxView;
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.tiktok.settings.preference.ReVancedPreferenceFragment;
|
||||
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
/**
|
||||
* Hooks AdPersonalizationActivity.
|
||||
* <p>
|
||||
* This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity.
|
||||
*
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class AdPersonalizationActivityHook {
|
||||
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
|
||||
try {
|
||||
Class<?> entryClazz = Class.forName(entryClazzName);
|
||||
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
|
||||
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
|
||||
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
|
||||
Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
|
||||
return entryConstructor.newInstance(buttonInfo);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
|
||||
InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Initialize the settings menu.
|
||||
* @param base The activity to initialize the settings menu on.
|
||||
* @return Whether the settings menu should be initialized.
|
||||
*/
|
||||
public static boolean initialize(AdPersonalizationActivity base) {
|
||||
Bundle extras = base.getIntent().getExtras();
|
||||
if (extras != null && !extras.getBoolean("revanced", false)) return false;
|
||||
|
||||
SettingsStatus.load();
|
||||
|
||||
LinearLayout linearLayout = new LinearLayout(base);
|
||||
linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
|
||||
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
linearLayout.setFitsSystemWindows(true);
|
||||
linearLayout.setTransitionGroup(true);
|
||||
|
||||
FrameLayout fragment = new FrameLayout(base);
|
||||
fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1));
|
||||
int fragmentId = View.generateViewId();
|
||||
fragment.setId(fragmentId);
|
||||
|
||||
linearLayout.addView(fragment);
|
||||
base.setContentView(linearLayout);
|
||||
|
||||
PreferenceFragment preferenceFragment = new ReVancedPreferenceFragment();
|
||||
base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void startSettingsActivity() {
|
||||
Context appContext = Utils.getContext();
|
||||
if (appContext != null) {
|
||||
Intent intent = new Intent(appContext, AdPersonalizationActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("revanced", true);
|
||||
appContext.startActivity(intent);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Utils.getContext() return null");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.FloatSetting;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true);
|
||||
public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true);
|
||||
public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true);
|
||||
public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true);
|
||||
public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true);
|
||||
public static final StringSetting MIN_MAX_LIKES = new StringSetting("min_max_likes", "0-" + Long.MAX_VALUE, true);
|
||||
public static final StringSetting DOWNLOAD_PATH = new StringSetting("down_path", "DCIM/TikTok");
|
||||
public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE);
|
||||
public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE);
|
||||
public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f);
|
||||
public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true);
|
||||
public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us");
|
||||
public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160");
|
||||
public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile");
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings;
|
||||
|
||||
public class SettingsStatus {
|
||||
public static boolean feedFilterEnabled = false;
|
||||
public static boolean downloadEnabled = false;
|
||||
public static boolean simSpoofEnabled = false;
|
||||
|
||||
public static void enableFeedFilter() {
|
||||
feedFilterEnabled = true;
|
||||
}
|
||||
|
||||
public static void enableDownload() {
|
||||
downloadEnabled = true;
|
||||
}
|
||||
|
||||
public static void enableSimSpoof() {
|
||||
simSpoofEnabled = true;
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Environment;
|
||||
import android.preference.DialogPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class DownloadPathPreference extends DialogPreference {
|
||||
private final Context context;
|
||||
private final String[] entryValues = {"DCIM", "Movies", "Pictures"};
|
||||
private String mValue;
|
||||
|
||||
private boolean mValueSet;
|
||||
private int mediaPathIndex;
|
||||
private String childDownloadPath;
|
||||
|
||||
public DownloadPathPreference(Context context, String title, StringSetting setting) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.setTitle(title);
|
||||
this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get());
|
||||
this.setKey(setting.key);
|
||||
this.setValue(setting.get());
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.mValue;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
final boolean changed = !TextUtils.equals(mValue, value);
|
||||
if (changed || !mValueSet) {
|
||||
mValue = value;
|
||||
mValueSet = true;
|
||||
persistString(value);
|
||||
if (changed) {
|
||||
notifyDependencyChange(shouldDisableDependents());
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
String currentMedia = getValue().split("/")[0];
|
||||
childDownloadPath = getValue().substring(getValue().indexOf("/") + 1);
|
||||
mediaPathIndex = findIndexOf(currentMedia);
|
||||
|
||||
LinearLayout dialogView = new LinearLayout(context);
|
||||
RadioGroup mediaPath = new RadioGroup(context);
|
||||
mediaPath.setLayoutParams(new RadioGroup.LayoutParams(-1, -2));
|
||||
for (String entryValue : entryValues) {
|
||||
RadioButton radioButton = new RadioButton(context);
|
||||
radioButton.setText(entryValue);
|
||||
radioButton.setId(View.generateViewId());
|
||||
mediaPath.addView(radioButton);
|
||||
}
|
||||
mediaPath.setOnCheckedChangeListener((radioGroup, id) -> {
|
||||
RadioButton radioButton = radioGroup.findViewById(id);
|
||||
mediaPathIndex = findIndexOf(radioButton.getText().toString());
|
||||
});
|
||||
mediaPath.check(mediaPath.getChildAt(mediaPathIndex).getId());
|
||||
EditText downloadPath = new EditText(context);
|
||||
downloadPath.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
downloadPath.setText(childDownloadPath);
|
||||
downloadPath.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
childDownloadPath = editable.toString();
|
||||
}
|
||||
});
|
||||
dialogView.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
|
||||
dialogView.setOrientation(LinearLayout.VERTICAL);
|
||||
dialogView.addView(mediaPath);
|
||||
dialogView.addView(downloadPath);
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setTitle("Download Path");
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult && mediaPathIndex >= 0) {
|
||||
String newValue = entryValues[mediaPathIndex] + "/" + childDownloadPath;
|
||||
setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + newValue);
|
||||
setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private int findIndexOf(String str) {
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
if (str.equals(entryValues[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
public class InputTextPreference extends EditTextPreference {
|
||||
|
||||
public InputTextPreference(Context context, String title, String summary, StringSetting setting) {
|
||||
super(context);
|
||||
this.setTitle(title);
|
||||
this.setSummary(summary);
|
||||
this.setKey(setting.key);
|
||||
this.setText(setting.get());
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.preference.DialogPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class RangeValuePreference extends DialogPreference {
|
||||
private final Context context;
|
||||
|
||||
private String minValue;
|
||||
|
||||
private String maxValue;
|
||||
|
||||
private String mValue;
|
||||
|
||||
private boolean mValueSet;
|
||||
|
||||
public RangeValuePreference(Context context, String title, String summary, StringSetting setting) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
setTitle(title);
|
||||
setSummary(summary);
|
||||
setKey(setting.key);
|
||||
setValue(setting.get());
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
final boolean changed = !TextUtils.equals(mValue, value);
|
||||
if (changed || !mValueSet) {
|
||||
mValue = value;
|
||||
mValueSet = true;
|
||||
persistString(value);
|
||||
if (changed) {
|
||||
notifyDependencyChange(shouldDisableDependents());
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
minValue = getValue().split("-")[0];
|
||||
maxValue = getValue().split("-")[1];
|
||||
LinearLayout dialogView = new LinearLayout(context);
|
||||
dialogView.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout minView = new LinearLayout(context);
|
||||
minView.setOrientation(LinearLayout.HORIZONTAL);
|
||||
TextView min = new TextView(context);
|
||||
min.setText("Min: ");
|
||||
minView.addView(min);
|
||||
EditText minEditText = new EditText(context);
|
||||
minEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
minEditText.setText(minValue);
|
||||
minView.addView(minEditText);
|
||||
dialogView.addView(minView);
|
||||
LinearLayout maxView = new LinearLayout(context);
|
||||
maxView.setOrientation(LinearLayout.HORIZONTAL);
|
||||
TextView max = new TextView(context);
|
||||
max.setText("Max: ");
|
||||
maxView.addView(max);
|
||||
EditText maxEditText = new EditText(context);
|
||||
maxEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
maxEditText.setText(maxValue);
|
||||
maxView.addView(maxEditText);
|
||||
dialogView.addView(maxView);
|
||||
minEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
minValue = editable.toString();
|
||||
}
|
||||
});
|
||||
maxEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
maxValue = editable.toString();
|
||||
}
|
||||
});
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
String newValue = minValue + "-" + maxValue;
|
||||
setValue(newValue);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceScreen;
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.DownloadsPreferenceCategory;
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.FeedFilterPreferenceCategory;
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.ExtensionPreferenceCategory;
|
||||
import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPreferenceCategory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Preference fragment for ReVanced settings
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
|
||||
@Override
|
||||
protected void syncSettingWithPreference(@NonNull @NotNull Preference pref,
|
||||
@NonNull @NotNull Setting<?> setting,
|
||||
boolean applySettingToPreference) {
|
||||
if (pref instanceof RangeValuePreference) {
|
||||
RangeValuePreference rangeValuePref = (RangeValuePreference) pref;
|
||||
Setting.privateSetValueFromString(setting, rangeValuePref.getValue());
|
||||
} else if (pref instanceof DownloadPathPreference) {
|
||||
DownloadPathPreference downloadPathPref = (DownloadPathPreference) pref;
|
||||
Setting.privateSetValueFromString(setting, downloadPathPref.getValue());
|
||||
} else {
|
||||
super.syncSettingWithPreference(pref, setting, applySettingToPreference);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
final var context = getContext();
|
||||
|
||||
// Currently no resources can be compiled for TikTok (fails with aapt error).
|
||||
// So all TikTok Strings are hard coded in the extension.
|
||||
restartDialogTitle = "Refresh and restart";
|
||||
restartDialogButtonText = "Restart";
|
||||
confirmDialogTitle = "Do you wish to proceed?";
|
||||
|
||||
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
|
||||
setPreferenceScreen(preferenceScreen);
|
||||
|
||||
// Custom categories reference app specific Settings class.
|
||||
new FeedFilterPreferenceCategory(context, preferenceScreen);
|
||||
new DownloadsPreferenceCategory(context, preferenceScreen);
|
||||
new SimSpoofPreferenceCategory(context, preferenceScreen);
|
||||
new ExtensionPreferenceCategory(context, preferenceScreen);
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
||||
|
||||
/**
|
||||
* Because resources cannot be added to TikTok,
|
||||
* these strings are copied from the shared strings.xml file.
|
||||
*
|
||||
* Changes here must also be made in strings.xml
|
||||
*/
|
||||
private final Map<String, String> aboutStrings = Map.of(
|
||||
"revanced_settings_about_links_body", "You are using ReVanced Patches version <i>%s</i>",
|
||||
"revanced_settings_about_links_dev_header", "Note",
|
||||
"revanced_settings_about_links_dev_body", "This version is a pre-release and you may experience unexpected issues",
|
||||
"revanced_settings_about_links_header", "Official links"
|
||||
);
|
||||
|
||||
{
|
||||
//noinspection deprecation
|
||||
setTitle("About");
|
||||
}
|
||||
|
||||
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ReVancedTikTokAboutPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getString(String key, Object ... args) {
|
||||
String format = aboutStrings.get(key);
|
||||
|
||||
if (format == null) {
|
||||
Logger.printException(() -> "Unknown key: " + key);
|
||||
return "";
|
||||
}
|
||||
|
||||
return String.format(format, args);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class TogglePreference extends SwitchPreference {
|
||||
public TogglePreference(Context context, String title, String summary, BooleanSetting setting) {
|
||||
super(context);
|
||||
this.setTitle(title);
|
||||
this.setSummary(summary);
|
||||
this.setKey(setting.key);
|
||||
this.setChecked(setting.get());
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference.categories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceScreen;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public abstract class ConditionalPreferenceCategory extends PreferenceCategory {
|
||||
public ConditionalPreferenceCategory(Context context, PreferenceScreen screen) {
|
||||
super(context);
|
||||
|
||||
if (getSettingsStatus()) {
|
||||
screen.addPreference(this);
|
||||
addPreferences(context);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract boolean getSettingsStatus();
|
||||
|
||||
public abstract void addPreferences(Context context);
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference.categories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceScreen;
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import app.revanced.extension.tiktok.settings.SettingsStatus;
|
||||
import app.revanced.extension.tiktok.settings.preference.DownloadPathPreference;
|
||||
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory {
|
||||
public DownloadsPreferenceCategory(Context context, PreferenceScreen screen) {
|
||||
super(context, screen);
|
||||
setTitle("Downloads");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getSettingsStatus() {
|
||||
return SettingsStatus.downloadEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPreferences(Context context) {
|
||||
addPreference(new DownloadPathPreference(
|
||||
context,
|
||||
"Download path",
|
||||
Settings.DOWNLOAD_PATH
|
||||
));
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Remove watermark", "",
|
||||
Settings.DOWNLOAD_WATERMARK
|
||||
));
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference.categories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceScreen;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.tiktok.settings.preference.ReVancedTikTokAboutPreference;
|
||||
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory {
|
||||
public ExtensionPreferenceCategory(Context context, PreferenceScreen screen) {
|
||||
super(context, screen);
|
||||
setTitle("Miscellaneous");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getSettingsStatus() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPreferences(Context context) {
|
||||
addPreference(new ReVancedTikTokAboutPreference(context));
|
||||
|
||||
addPreference(new TogglePreference(context,
|
||||
"Enable debug log",
|
||||
"Show extension debug log.",
|
||||
BaseSettings.DEBUG
|
||||
));
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference.categories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceScreen;
|
||||
import app.revanced.extension.tiktok.settings.preference.RangeValuePreference;
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import app.revanced.extension.tiktok.settings.SettingsStatus;
|
||||
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory {
|
||||
public FeedFilterPreferenceCategory(Context context, PreferenceScreen screen) {
|
||||
super(context, screen);
|
||||
setTitle("Feed filter");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getSettingsStatus() {
|
||||
return SettingsStatus.feedFilterEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPreferences(Context context) {
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Remove feed ads", "Remove ads from feed.",
|
||||
Settings.REMOVE_ADS
|
||||
));
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Hide livestreams", "Hide livestreams from feed.",
|
||||
Settings.HIDE_LIVE
|
||||
));
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Hide story", "Hide story from feed.",
|
||||
Settings.HIDE_STORY
|
||||
));
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Hide image video", "Hide image video from feed.",
|
||||
Settings.HIDE_IMAGE
|
||||
));
|
||||
addPreference(new RangeValuePreference(
|
||||
context,
|
||||
"Min/Max views", "The minimum or maximum views of a video to show.",
|
||||
Settings.MIN_MAX_VIEWS
|
||||
));
|
||||
addPreference(new RangeValuePreference(
|
||||
context,
|
||||
"Min/Max likes", "The minimum or maximum likes of a video to show.",
|
||||
Settings.MIN_MAX_LIKES
|
||||
));
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package app.revanced.extension.tiktok.settings.preference.categories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceScreen;
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
import app.revanced.extension.tiktok.settings.SettingsStatus;
|
||||
import app.revanced.extension.tiktok.settings.preference.InputTextPreference;
|
||||
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory {
|
||||
public SimSpoofPreferenceCategory(Context context, PreferenceScreen screen) {
|
||||
super(context, screen);
|
||||
setTitle("Bypass regional restriction");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean getSettingsStatus() {
|
||||
return SettingsStatus.simSpoofEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPreferences(Context context) {
|
||||
addPreference(new TogglePreference(
|
||||
context,
|
||||
"Fake sim card info",
|
||||
"Bypass regional restriction by fake sim card information.",
|
||||
Settings.SIM_SPOOF
|
||||
));
|
||||
addPreference(new InputTextPreference(
|
||||
context,
|
||||
"Country ISO", "us, uk, jp, ...",
|
||||
Settings.SIM_SPOOF_ISO
|
||||
));
|
||||
addPreference(new InputTextPreference(
|
||||
context,
|
||||
"Operator mcc+mnc", "mcc+mnc",
|
||||
Settings.SIMSPOOF_MCCMNC
|
||||
));
|
||||
addPreference(new InputTextPreference(
|
||||
context,
|
||||
"Operator name", "Name of the operator.",
|
||||
Settings.SIMSPOOF_OP_NAME
|
||||
));
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package app.revanced.extension.tiktok.speed;
|
||||
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
|
||||
public class PlaybackSpeedPatch {
|
||||
public static void rememberPlaybackSpeed(float newSpeed) {
|
||||
Settings.REMEMBERED_SPEED.save(newSpeed);
|
||||
}
|
||||
|
||||
public static float getPlaybackSpeed() {
|
||||
return Settings.REMEMBERED_SPEED.get();
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package app.revanced.extension.tiktok.spoof.sim;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.tiktok.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofSimPatch {
|
||||
|
||||
private static final boolean ENABLED = Settings.SIM_SPOOF.get();
|
||||
|
||||
public static String getCountryIso(String value) {
|
||||
if (ENABLED) {
|
||||
String iso = Settings.SIM_SPOOF_ISO.get();
|
||||
Logger.printDebug(() -> "Spoofing sim ISO from: " + value + " to: " + iso);
|
||||
return iso;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String getOperator(String value) {
|
||||
if (ENABLED) {
|
||||
String mcc_mnc = Settings.SIMSPOOF_MCCMNC.get();
|
||||
Logger.printDebug(() -> "Spoofing sim MCC-MNC from: " + value + " to: " + mcc_mnc);
|
||||
return mcc_mnc;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String getOperatorName(String value) {
|
||||
if (ENABLED) {
|
||||
String operator = Settings.SIMSPOOF_OP_NAME.get();
|
||||
Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator);
|
||||
return operator;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package app.revanced.extension.tudortmund.lockscreen;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.os.Build;
|
||||
import android.view.Display;
|
||||
import android.view.Window;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
|
||||
|
||||
public class ShowOnLockscreenPatch {
|
||||
public static Window getWindow(AppCompatActivity activity, float brightness) {
|
||||
Window window = activity.getWindow();
|
||||
|
||||
if (brightness >= 0) {
|
||||
// High brightness set, therefore show on lockscreen.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(true);
|
||||
else window.addFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD);
|
||||
} else {
|
||||
// Ignore brightness reset when the screen is turned off.
|
||||
DisplayManager displayManager = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE);
|
||||
|
||||
boolean isScreenOn = false;
|
||||
for (Display display : displayManager.getDisplays()) {
|
||||
if (display.getState() == Display.STATE_OFF) continue;
|
||||
|
||||
isScreenOn = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isScreenOn) {
|
||||
// Hide on lockscreen.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(false);
|
||||
else window.clearFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD);
|
||||
}
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package app.revanced.extension.tumblr.patches;
|
||||
|
||||
import com.tumblr.rumblr.model.TimelineObject;
|
||||
import com.tumblr.rumblr.model.Timelineable;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public final class TimelineFilterPatch {
|
||||
private static final HashSet<String> blockedObjectTypes = new HashSet<>();
|
||||
|
||||
static {
|
||||
// This dummy gets removed by the TimelineFilterPatch and in its place,
|
||||
// equivalent instructions with a different constant string
|
||||
// will be inserted for each Timeline object type filter.
|
||||
// Modifying this line may break the patch.
|
||||
blockedObjectTypes.add("BLOCKED_OBJECT_DUMMY");
|
||||
}
|
||||
|
||||
// Calls to this method are injected where the list of Timeline objects is first received.
|
||||
// We modify the list filter out elements that we want to hide.
|
||||
public static void filterTimeline(final List<TimelineObject<? extends Timelineable>> timelineObjects) {
|
||||
final var iterator = timelineObjects.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
var timelineElement = iterator.next();
|
||||
if (timelineElement == null) continue;
|
||||
|
||||
String elementType = timelineElement.getData().getTimelineObjectType().toString();
|
||||
if (blockedObjectTypes.contains(elementType)) iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package app.revanced.extension.twitch;
|
||||
|
||||
public class Utils {
|
||||
|
||||
/* Called from SettingsPatch smali */
|
||||
public static int getStringId(String name) {
|
||||
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "string");
|
||||
}
|
||||
|
||||
/* Called from SettingsPatch smali */
|
||||
public static int getDrawableId(String name) {
|
||||
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "drawable");
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package app.revanced.extension.twitch.adblock;
|
||||
|
||||
import okhttp3.Request;
|
||||
|
||||
public interface IAdblockService {
|
||||
String friendlyName();
|
||||
|
||||
Integer maxAttempts();
|
||||
|
||||
Boolean isAvailable();
|
||||
|
||||
Request rewriteHlsRequest(Request originalRequest);
|
||||
|
||||
static boolean isVod(Request request){
|
||||
return request.url().pathSegments().contains("vod");
|
||||
}
|
||||
|
||||
static String channelName(Request request) {
|
||||
for (String pathSegment : request.url().pathSegments()) {
|
||||
if (pathSegment.endsWith(".m3u8")) {
|
||||
return pathSegment.replace(".m3u8", "");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package app.revanced.extension.twitch.adblock;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
public class LuminousService implements IAdblockService {
|
||||
@Override
|
||||
public String friendlyName() {
|
||||
return str("revanced_proxy_luminous");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer maxAttempts() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Request rewriteHlsRequest(Request originalRequest) {
|
||||
var type = IAdblockService.isVod(originalRequest) ? "vod" : "playlist";
|
||||
var url = HttpUrl.parse("https://eu.luminous.dev/" +
|
||||
type +
|
||||
"/" +
|
||||
IAdblockService.channelName(originalRequest) +
|
||||
".m3u8" +
|
||||
"%3Fallow_source%3Dtrue%26allow_audio_only%3Dtrue%26fast_bread%3Dtrue"
|
||||
);
|
||||
|
||||
if (url == null) {
|
||||
Logger.printException(() -> "Failed to parse rewritten URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overwrite old request
|
||||
return new Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package app.revanced.extension.twitch.adblock;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.twitch.api.RetrofitClient;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
public class PurpleAdblockService implements IAdblockService {
|
||||
private final Map<String, Boolean> tunnels = new HashMap<>() {{
|
||||
put("https://eu1.jupter.ga", false);
|
||||
put("https://eu2.jupter.ga", false);
|
||||
}};
|
||||
|
||||
@Override
|
||||
public String friendlyName() {
|
||||
return str("revanced_proxy_purpleadblock");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer maxAttempts() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isAvailable() {
|
||||
for (String tunnel : tunnels.keySet()) {
|
||||
var success = true;
|
||||
|
||||
try {
|
||||
var response = RetrofitClient.getInstance().getPurpleAdblockApi().ping(tunnel).execute();
|
||||
if (!response.isSuccessful()) {
|
||||
Logger.printException(() ->
|
||||
"PurpleAdBlock tunnel $tunnel returned an error: HTTP code " + response.code()
|
||||
);
|
||||
Logger.printDebug(response::message);
|
||||
|
||||
try (var errorBody = response.errorBody()) {
|
||||
if (errorBody != null) {
|
||||
Logger.printDebug(() -> {
|
||||
try {
|
||||
return errorBody.string();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
success = false;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "PurpleAdBlock tunnel $tunnel is unavailable", ex);
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Cache availability data
|
||||
tunnels.put(tunnel, success);
|
||||
|
||||
if (success)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Request rewriteHlsRequest(Request originalRequest) {
|
||||
for (Map.Entry<String, Boolean> entry : tunnels.entrySet()) {
|
||||
if (!entry.getValue()) continue;
|
||||
|
||||
var server = entry.getKey();
|
||||
|
||||
// Compose new URL
|
||||
var url = HttpUrl.parse(server + "/channel/" + IAdblockService.channelName(originalRequest));
|
||||
if (url == null) {
|
||||
Logger.printException(() -> "Failed to parse rewritten URL");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overwrite old request
|
||||
return new Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
.build();
|
||||
}
|
||||
|
||||
Logger.printException(() -> "No tunnels are available");
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package app.revanced.extension.twitch.api;
|
||||
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Url;
|
||||
|
||||
/* only used for service pings */
|
||||
public interface PurpleAdblockApi {
|
||||
@GET /* root */
|
||||
Call<ResponseBody> ping(@Url String baseUrl);
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
package app.revanced.extension.twitch.api;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.twitch.adblock.IAdblockService;
|
||||
import app.revanced.extension.twitch.adblock.LuminousService;
|
||||
import app.revanced.extension.twitch.adblock.PurpleAdblockService;
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
public class RequestInterceptor implements Interceptor {
|
||||
private IAdblockService activeService = null;
|
||||
|
||||
private static final String PROXY_DISABLED = str("revanced_block_embedded_ads_entry_1");
|
||||
private static final String LUMINOUS_SERVICE = str("revanced_block_embedded_ads_entry_2");
|
||||
private static final String PURPLE_ADBLOCK_SERVICE = str("revanced_block_embedded_ads_entry_3");
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Response intercept(@NonNull Chain chain) throws IOException {
|
||||
var originalRequest = chain.request();
|
||||
|
||||
if (Settings.BLOCK_EMBEDDED_ADS.get().equals(PROXY_DISABLED)) {
|
||||
return chain.proceed(originalRequest);
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Intercepted request to URL:" + originalRequest.url());
|
||||
|
||||
// Skip if not HLS manifest request
|
||||
if (!originalRequest.url().host().contains("usher.ttvnw.net")) {
|
||||
return chain.proceed(originalRequest);
|
||||
}
|
||||
|
||||
final String isVod;
|
||||
if (IAdblockService.isVod(originalRequest)) isVod = "yes";
|
||||
else isVod = "no";
|
||||
|
||||
Logger.printDebug(() -> "Found HLS manifest request. Is VOD? " +
|
||||
isVod +
|
||||
"; Channel: " +
|
||||
IAdblockService.channelName(originalRequest)
|
||||
);
|
||||
|
||||
// None of the services support VODs currently
|
||||
if (IAdblockService.isVod(originalRequest)) return chain.proceed(originalRequest);
|
||||
|
||||
updateActiveService();
|
||||
|
||||
if (activeService != null) {
|
||||
var available = activeService.isAvailable();
|
||||
var rewritten = activeService.rewriteHlsRequest(originalRequest);
|
||||
|
||||
|
||||
if (!available || rewritten == null) {
|
||||
Utils.showToastShort(String.format(
|
||||
str("revanced_embedded_ads_service_unavailable"), activeService.friendlyName()
|
||||
));
|
||||
return chain.proceed(originalRequest);
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Rewritten HLS stream URL: " + rewritten.url());
|
||||
|
||||
var maxAttempts = activeService.maxAttempts();
|
||||
|
||||
for (var i = 1; i <= maxAttempts; i++) {
|
||||
// Execute rewritten request and close body to allow multiple proceed() calls
|
||||
var response = chain.proceed(rewritten);
|
||||
response.close();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
int attempt = i;
|
||||
Logger.printException(() -> "Request failed (attempt " +
|
||||
attempt +
|
||||
"/" + maxAttempts + "): HTTP error " +
|
||||
response.code() +
|
||||
" (" + response.message() + ")"
|
||||
);
|
||||
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
Logger.printException(() -> "Failed to sleep", e);
|
||||
}
|
||||
} else {
|
||||
// Accept response from ad blocker
|
||||
Logger.printDebug(() -> "Ad-blocker used");
|
||||
return chain.proceed(rewritten);
|
||||
}
|
||||
}
|
||||
|
||||
// maxAttempts exceeded; giving up on using the ad blocker
|
||||
Utils.showToastLong(String.format(
|
||||
str("revanced_embedded_ads_service_failed"),
|
||||
activeService.friendlyName())
|
||||
);
|
||||
}
|
||||
|
||||
// Adblock disabled
|
||||
return chain.proceed(originalRequest);
|
||||
|
||||
}
|
||||
|
||||
private void updateActiveService() {
|
||||
var current = Settings.BLOCK_EMBEDDED_ADS.get();
|
||||
|
||||
if (current.equals(LUMINOUS_SERVICE) && !(activeService instanceof LuminousService))
|
||||
activeService = new LuminousService();
|
||||
else if (current.equals(PURPLE_ADBLOCK_SERVICE) && !(activeService instanceof PurpleAdblockService))
|
||||
activeService = new PurpleAdblockService();
|
||||
else if (current.equals(PROXY_DISABLED))
|
||||
activeService = null;
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package app.revanced.extension.twitch.api;
|
||||
|
||||
import retrofit2.Retrofit;
|
||||
|
||||
public class RetrofitClient {
|
||||
|
||||
private static RetrofitClient instance = null;
|
||||
private final PurpleAdblockApi purpleAdblockApi;
|
||||
|
||||
private RetrofitClient() {
|
||||
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build();
|
||||
purpleAdblockApi = retrofit.create(PurpleAdblockApi.class);
|
||||
}
|
||||
|
||||
public static synchronized RetrofitClient getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RetrofitClient();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public PurpleAdblockApi getPurpleAdblockApi() {
|
||||
return purpleAdblockApi;
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.twitch.patches;
|
||||
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AudioAdsPatch {
|
||||
public static boolean shouldBlockAudioAds() {
|
||||
return Settings.BLOCK_AUDIO_ADS.get();
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.twitch.patches;
|
||||
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AutoClaimChannelPointsPatch {
|
||||
public static boolean shouldAutoClaim() {
|
||||
return Settings.AUTO_CLAIM_CHANNEL_POINTS.get();
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.twitch.patches;
|
||||
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DebugModePatch {
|
||||
public static boolean isDebugModeEnabled() {
|
||||
return Settings.TWITCH_DEBUG_MODE.get();
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.twitch.patches;
|
||||
|
||||
import app.revanced.extension.twitch.api.RequestInterceptor;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class EmbeddedAdsPatch {
|
||||
public static RequestInterceptor createRequestInterceptor() {
|
||||
return new RequestInterceptor();
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package app.revanced.extension.twitch.patches;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.SpannedString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
import tv.twitch.android.shared.chat.util.ClickableUsernameSpan;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ShowDeletedMessagesPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean shouldUseSpoiler() {
|
||||
return "spoiler".equals(Settings.SHOW_DELETED_MESSAGES.get());
|
||||
}
|
||||
|
||||
public static boolean shouldCrossOut() {
|
||||
return "cross-out".equals(Settings.SHOW_DELETED_MESSAGES.get());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Spanned reformatDeletedMessage(Spanned original) {
|
||||
if (!shouldCrossOut())
|
||||
return null;
|
||||
|
||||
SpannableStringBuilder ssb = new SpannableStringBuilder(original);
|
||||
ssb.setSpan(new StrikethroughSpan(), 0, original.length(), 0);
|
||||
ssb.append(" (").append(str("revanced_deleted_msg")).append(")");
|
||||
ssb.setSpan(new StyleSpan(Typeface.ITALIC), original.length(), ssb.length(), 0);
|
||||
|
||||
// Gray-out username
|
||||
ClickableUsernameSpan[] usernameSpans = original.getSpans(0, original.length(), ClickableUsernameSpan.class);
|
||||
if (usernameSpans.length > 0) {
|
||||
ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#ADADB8")), 0, original.getSpanEnd(usernameSpans[0]), 0);
|
||||
}
|
||||
|
||||
return new SpannedString(ssb);
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.twitch.patches;
|
||||
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class VideoAdsPatch {
|
||||
public static boolean shouldBlockVideoAds() {
|
||||
return Settings.BLOCK_VIDEO_ADS.get();
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package app.revanced.extension.twitch.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.twitch.settings.preference.ReVancedPreferenceFragment;
|
||||
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
|
||||
import tv.twitch.android.settings.SettingsActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hooks AppCompatActivity.
|
||||
* <p>
|
||||
* This class is responsible for injecting our own fragment by replacing the AppCompatActivity.
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class AppCompatActivityHook {
|
||||
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
|
||||
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
|
||||
|
||||
/**
|
||||
* Launches SettingsActivity and show ReVanced settings
|
||||
*/
|
||||
public static void startSettingsActivity() {
|
||||
Logger.printDebug(() -> "Launching ReVanced settings");
|
||||
|
||||
final var context = Utils.getContext();
|
||||
|
||||
if (context != null) {
|
||||
Intent intent = new Intent(context, SettingsActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(EXTRA_REVANCED_SETTINGS, true);
|
||||
intent.putExtras(bundle);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for easy access in smali
|
||||
* @return Returns string resource id
|
||||
*/
|
||||
public static int getReVancedSettingsString() {
|
||||
return app.revanced.extension.twitch.Utils.getStringId("revanced_settings");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated
|
||||
* @return Returns a modified list of menu groups
|
||||
*/
|
||||
public static List<SettingsMenuGroup> handleSettingMenuCreation(List<SettingsMenuGroup> settingGroups, Object revancedEntry) {
|
||||
List<SettingsMenuGroup> groups = new ArrayList<>(settingGroups);
|
||||
|
||||
if (groups.isEmpty()) {
|
||||
// Create new menu group if none exist yet
|
||||
List<Object> items = new ArrayList<>();
|
||||
items.add(revancedEntry);
|
||||
groups.add(new SettingsMenuGroup(items));
|
||||
} else {
|
||||
// Add to last menu group
|
||||
int groupIdx = groups.size() - 1;
|
||||
List<Object> items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
|
||||
items.add(revancedEntry);
|
||||
groups.add(new SettingsMenuGroup(items));
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> settingGroups.size() + " menu groups in list");
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts settings menu group onclick events
|
||||
* @return Returns true if handled, otherwise false
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static boolean handleSettingMenuOnClick(Enum item) {
|
||||
Logger.printDebug(() -> "item " + item.ordinal() + " clicked");
|
||||
if (item.ordinal() != REVANCED_SETTINGS_MENU_ITEM_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
startSettingsActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts fragment loading in SettingsActivity.onCreate
|
||||
* @return Returns true if the revanced settings have been requested by the user, otherwise false
|
||||
*/
|
||||
public static boolean handleSettingsCreation(androidx.appcompat.app.AppCompatActivity base) {
|
||||
if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
|
||||
Logger.printDebug(() -> "Revanced settings not requested");
|
||||
return false; // User wants to enter another settings fragment
|
||||
}
|
||||
Logger.printDebug(() -> "ReVanced settings requested");
|
||||
|
||||
ReVancedPreferenceFragment fragment = new ReVancedPreferenceFragment();
|
||||
ActionBar supportActionBar = base.getSupportActionBar();
|
||||
if (supportActionBar != null)
|
||||
supportActionBar.setTitle(app.revanced.extension.twitch.Utils.getStringId("revanced_settings"));
|
||||
|
||||
base.getFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(Utils.getResourceIdentifier("fragment_container", "id"), fragment)
|
||||
.commit();
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package app.revanced.extension.twitch.settings;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
public class Settings extends BaseSettings {
|
||||
/* Ads */
|
||||
public static final BooleanSetting BLOCK_VIDEO_ADS = new BooleanSetting("revanced_block_video_ads", TRUE);
|
||||
public static final BooleanSetting BLOCK_AUDIO_ADS = new BooleanSetting("revanced_block_audio_ads", TRUE);
|
||||
public static final StringSetting BLOCK_EMBEDDED_ADS = new StringSetting("revanced_block_embedded_ads", "luminous");
|
||||
|
||||
/* Chat */
|
||||
public static final StringSetting SHOW_DELETED_MESSAGES = new StringSetting("revanced_show_deleted_messages", "cross-out");
|
||||
public static final BooleanSetting AUTO_CLAIM_CHANNEL_POINTS = new BooleanSetting("revanced_auto_claim_channel_points", TRUE);
|
||||
|
||||
/* Misc */
|
||||
/**
|
||||
* Not to be confused with {@link BaseSettings#DEBUG}.
|
||||
*/
|
||||
public static final BooleanSetting TWITCH_DEBUG_MODE = new BooleanSetting("revanced_twitch_debug_mode", FALSE, true);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package app.revanced.extension.twitch.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class CustomPreferenceCategory extends PreferenceCategory {
|
||||
public CustomPreferenceCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View rootView) {
|
||||
super.onBindView(rootView);
|
||||
|
||||
if(rootView instanceof TextView) {
|
||||
((TextView) rootView).setTextColor(Color.parseColor("#8161b3"));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package app.revanced.extension.twitch.settings.preference;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
||||
import app.revanced.extension.twitch.settings.Settings;
|
||||
|
||||
/**
|
||||
* Preference fragment for ReVanced settings
|
||||
*/
|
||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
super.initialize();
|
||||
|
||||
// Do anything that forces this apps Settings bundle to load.
|
||||
if (Settings.BLOCK_VIDEO_ADS.get()) {
|
||||
Logger.printDebug(() -> "Block video ads enabled"); // Any statement that references the app settings.
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.json
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
abstract class BaseJsonHook : JsonHook {
|
||||
abstract fun apply(json: JSONObject)
|
||||
|
||||
override fun transform(json: JSONObject) = json.apply { apply(json) }
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.json
|
||||
|
||||
import app.revanced.extension.twitter.patches.hook.patch.Hook
|
||||
import org.json.JSONObject
|
||||
|
||||
interface JsonHook : Hook<JSONObject> {
|
||||
/**
|
||||
* Transform a JSONObject.
|
||||
*
|
||||
* @param json The JSONObject.
|
||||
*/
|
||||
fun transform(json: JSONObject): JSONObject
|
||||
|
||||
override fun hook(type: JSONObject) = transform(type)
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.json
|
||||
|
||||
import app.revanced.extension.twitter.patches.hook.patch.dummy.DummyHook
|
||||
import app.revanced.extension.twitter.utils.json.JsonUtils.parseJson
|
||||
import app.revanced.extension.twitter.utils.stream.StreamUtils
|
||||
import org.json.JSONException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object JsonHookPatch {
|
||||
// Additional hooks added by corresponding patch.
|
||||
private val hooks = buildList<JsonHook> {
|
||||
add(DummyHook)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseJsonHook(jsonInputStream: InputStream): InputStream {
|
||||
var jsonObject = try {
|
||||
parseJson(jsonInputStream)
|
||||
} catch (ignored: IOException) {
|
||||
return jsonInputStream // Unreachable.
|
||||
} catch (ignored: JSONException) {
|
||||
return jsonInputStream
|
||||
}
|
||||
|
||||
for (hook in hooks) jsonObject = hook.hook(jsonObject)
|
||||
|
||||
return StreamUtils.fromString(jsonObject.toString())
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.patch
|
||||
|
||||
interface Hook<T> {
|
||||
/**
|
||||
* Hook the given type.
|
||||
* @param type The type to hook
|
||||
*/
|
||||
fun hook(type: T): T
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.patch.ads
|
||||
|
||||
import app.revanced.extension.twitter.patches.hook.json.BaseJsonHook
|
||||
import app.revanced.extension.twitter.patches.hook.twifucker.TwiFucker
|
||||
import org.json.JSONObject
|
||||
|
||||
@Suppress("unused")
|
||||
object HideAdsHook : BaseJsonHook() {
|
||||
/**
|
||||
* Strips JSONObject from promoted ads.
|
||||
*
|
||||
* @param json The JSONObject.
|
||||
*/
|
||||
override fun apply(json: JSONObject) = TwiFucker.hidePromotedAds(json)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.patch.dummy
|
||||
|
||||
import app.revanced.extension.twitter.patches.hook.json.BaseJsonHook
|
||||
import app.revanced.extension.twitter.patches.hook.json.JsonHookPatch
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Dummy hook to reserve a register in [JsonHookPatch.hooks] list.
|
||||
*/
|
||||
object DummyHook : BaseJsonHook() {
|
||||
override fun apply(json: JSONObject) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.patch.recommendation
|
||||
|
||||
import app.revanced.extension.twitter.patches.hook.json.BaseJsonHook
|
||||
import app.revanced.extension.twitter.patches.hook.twifucker.TwiFucker
|
||||
import org.json.JSONObject
|
||||
|
||||
object RecommendedUsersHook : BaseJsonHook() {
|
||||
/**
|
||||
* Strips JSONObject from recommended users.
|
||||
*
|
||||
* @param json The JSONObject.
|
||||
*/
|
||||
override fun apply(json: JSONObject) = TwiFucker.hideRecommendedUsers(json)
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.twifucker
|
||||
|
||||
import android.util.Log
|
||||
import app.revanced.extension.twitter.patches.hook.twifucker.TwiFuckerUtils.forEach
|
||||
import app.revanced.extension.twitter.patches.hook.twifucker.TwiFuckerUtils.forEachIndexed
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
// https://raw.githubusercontent.com/Dr-TSNG/TwiFucker/880cdf1c1622e54ab45561ffcb4f53d94ed97bae/app/src/main/java/icu/nullptr/twifucker/hook/JsonHook.kt
|
||||
internal object TwiFucker {
|
||||
// root
|
||||
private fun JSONObject.jsonGetInstructions(): JSONArray? = optJSONObject("timeline")?.optJSONArray("instructions")
|
||||
|
||||
private fun JSONObject.jsonGetData(): JSONObject? = optJSONObject("data")
|
||||
|
||||
private fun JSONObject.jsonHasRecommendedUsers(): Boolean = has("recommended_users")
|
||||
|
||||
private fun JSONObject.jsonRemoveRecommendedUsers() {
|
||||
remove("recommended_users")
|
||||
}
|
||||
|
||||
private fun JSONObject.jsonCheckAndRemoveRecommendedUsers() {
|
||||
if (jsonHasRecommendedUsers()) {
|
||||
Log.d("ReVanced", "Handle recommended users: $this")
|
||||
jsonRemoveRecommendedUsers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.jsonHasThreads(): Boolean = has("threads")
|
||||
|
||||
private fun JSONObject.jsonRemoveThreads() {
|
||||
remove("threads")
|
||||
}
|
||||
|
||||
private fun JSONObject.jsonCheckAndRemoveThreads() {
|
||||
if (jsonHasThreads()) {
|
||||
Log.d("ReVanced", "Handle threads: $this")
|
||||
jsonRemoveThreads()
|
||||
}
|
||||
}
|
||||
|
||||
// data
|
||||
private fun JSONObject.dataGetInstructions(): JSONArray? {
|
||||
val timeline =
|
||||
optJSONObject("user_result")?.optJSONObject("result")
|
||||
?.optJSONObject("timeline_response")?.optJSONObject("timeline")
|
||||
?: optJSONObject("timeline_response")?.optJSONObject("timeline")
|
||||
?: optJSONObject("search")?.optJSONObject("timeline_response")?.optJSONObject("timeline")
|
||||
?: optJSONObject("timeline_response")
|
||||
return timeline?.optJSONArray("instructions")
|
||||
}
|
||||
|
||||
private fun JSONObject.dataCheckAndRemove() {
|
||||
dataGetInstructions()?.forEach { instruction ->
|
||||
instruction.instructionCheckAndRemove { it.entriesRemoveAnnoyance() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.dataGetLegacy(): JSONObject? =
|
||||
optJSONObject("tweet_result")?.optJSONObject("result")?.let {
|
||||
if (it.has("tweet")) {
|
||||
it.optJSONObject("tweet")
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}?.optJSONObject("legacy")
|
||||
|
||||
// entry
|
||||
private fun JSONObject.entryHasPromotedMetadata(): Boolean =
|
||||
optJSONObject("content")?.optJSONObject("item")?.optJSONObject("content")
|
||||
?.optJSONObject("tweet")
|
||||
?.has("promotedMetadata") == true || optJSONObject("content")?.optJSONObject("content")
|
||||
?.has("tweetPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content")
|
||||
?.has("tweetPromotedMetadata") == true
|
||||
|
||||
private fun JSONObject.entryGetContentItems(): JSONArray? =
|
||||
optJSONObject("content")?.optJSONArray("items")
|
||||
?: optJSONObject("content")?.optJSONObject("timelineModule")?.optJSONArray("items")
|
||||
|
||||
private fun JSONObject.entryIsTweetDetailRelatedTweets(): Boolean = optString("entryId").startsWith("tweetdetailrelatedtweets-")
|
||||
|
||||
private fun JSONObject.entryGetTrends(): JSONArray? = optJSONObject("content")?.optJSONObject("timelineModule")?.optJSONArray("items")
|
||||
|
||||
// trend
|
||||
private fun JSONObject.trendHasPromotedMetadata(): Boolean =
|
||||
optJSONObject("item")?.optJSONObject("content")?.optJSONObject("trend")
|
||||
?.has("promotedMetadata") == true
|
||||
|
||||
private fun JSONArray.trendRemoveAds() {
|
||||
val trendRemoveIndex = mutableListOf<Int>()
|
||||
forEachIndexed { trendIndex, trend ->
|
||||
if (trend.trendHasPromotedMetadata()) {
|
||||
Log.d("ReVanced", "Handle trends ads $trendIndex $trend")
|
||||
trendRemoveIndex.add(trendIndex)
|
||||
}
|
||||
}
|
||||
for (i in trendRemoveIndex.asReversed()) {
|
||||
remove(i)
|
||||
}
|
||||
}
|
||||
|
||||
// instruction
|
||||
private fun JSONObject.instructionTimelineAddEntries(): JSONArray? = optJSONArray("entries")
|
||||
|
||||
private fun JSONObject.instructionGetAddEntries(): JSONArray? = optJSONObject("addEntries")?.optJSONArray("entries")
|
||||
|
||||
private fun JSONObject.instructionCheckAndRemove(action: (JSONArray) -> Unit) {
|
||||
instructionTimelineAddEntries()?.let(action)
|
||||
instructionGetAddEntries()?.let(action)
|
||||
}
|
||||
|
||||
// entries
|
||||
private fun JSONArray.entriesRemoveTimelineAds() {
|
||||
val removeIndex = mutableListOf<Int>()
|
||||
forEachIndexed { entryIndex, entry ->
|
||||
entry.entryGetTrends()?.trendRemoveAds()
|
||||
|
||||
if (entry.entryHasPromotedMetadata()) {
|
||||
Log.d("ReVanced", "Handle timeline ads $entryIndex $entry")
|
||||
removeIndex.add(entryIndex)
|
||||
}
|
||||
|
||||
val innerRemoveIndex = mutableListOf<Int>()
|
||||
val contentItems = entry.entryGetContentItems()
|
||||
contentItems?.forEachIndexed inner@{ itemIndex, item ->
|
||||
if (item.entryHasPromotedMetadata()) {
|
||||
Log.d("ReVanced", "Handle timeline replies ads $entryIndex $entry")
|
||||
if (contentItems.length() == 1) {
|
||||
removeIndex.add(entryIndex)
|
||||
} else {
|
||||
innerRemoveIndex.add(itemIndex)
|
||||
}
|
||||
return@inner
|
||||
}
|
||||
}
|
||||
for (i in innerRemoveIndex.asReversed()) {
|
||||
contentItems?.remove(i)
|
||||
}
|
||||
}
|
||||
for (i in removeIndex.reversed()) {
|
||||
remove(i)
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONArray.entriesRemoveTweetDetailRelatedTweets() {
|
||||
val removeIndex = mutableListOf<Int>()
|
||||
forEachIndexed { entryIndex, entry ->
|
||||
|
||||
if (entry.entryIsTweetDetailRelatedTweets()) {
|
||||
Log.d("ReVanced", "Handle tweet detail related tweets $entryIndex $entry")
|
||||
removeIndex.add(entryIndex)
|
||||
}
|
||||
}
|
||||
for (i in removeIndex.reversed()) {
|
||||
remove(i)
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONArray.entriesRemoveAnnoyance() {
|
||||
entriesRemoveTimelineAds()
|
||||
entriesRemoveTweetDetailRelatedTweets()
|
||||
}
|
||||
|
||||
private fun JSONObject.entryIsWhoToFollow(): Boolean =
|
||||
optString("entryId").let {
|
||||
it.startsWith("whoToFollow-") || it.startsWith("who-to-follow-") || it.startsWith("connect-module-")
|
||||
}
|
||||
|
||||
private fun JSONObject.itemContainsPromotedUser(): Boolean =
|
||||
optJSONObject("item")?.optJSONObject("content")
|
||||
?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content")
|
||||
?.optJSONObject("user")
|
||||
?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content")
|
||||
?.optJSONObject("user")?.has("promotedMetadata") == true
|
||||
|
||||
fun JSONArray.entriesRemoveWhoToFollow() {
|
||||
val entryRemoveIndex = mutableListOf<Int>()
|
||||
forEachIndexed { entryIndex, entry ->
|
||||
if (!entry.entryIsWhoToFollow()) return@forEachIndexed
|
||||
|
||||
Log.d("ReVanced", "Handle whoToFollow $entryIndex $entry")
|
||||
entryRemoveIndex.add(entryIndex)
|
||||
|
||||
val items = entry.entryGetContentItems()
|
||||
val userRemoveIndex = mutableListOf<Int>()
|
||||
items?.forEachIndexed { index, item ->
|
||||
item.itemContainsPromotedUser().let {
|
||||
if (it) {
|
||||
Log.d("ReVanced", "Handle whoToFollow promoted user $index $item")
|
||||
userRemoveIndex.add(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i in userRemoveIndex.reversed()) {
|
||||
items?.remove(i)
|
||||
}
|
||||
}
|
||||
for (i in entryRemoveIndex.reversed()) {
|
||||
remove(i)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideRecommendedUsers(json: JSONObject) {
|
||||
json.filterInstructions { it.entriesRemoveWhoToFollow() }
|
||||
json.jsonCheckAndRemoveRecommendedUsers()
|
||||
}
|
||||
|
||||
fun hidePromotedAds(json: JSONObject) {
|
||||
json.filterInstructions { it.entriesRemoveAnnoyance() }
|
||||
json.jsonGetData()?.dataCheckAndRemove()
|
||||
}
|
||||
|
||||
private fun JSONObject.filterInstructions(action: (JSONArray) -> Unit) {
|
||||
jsonGetInstructions()?.forEach { instruction ->
|
||||
instruction.instructionCheckAndRemove(action)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.hook.twifucker
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
internal object TwiFuckerUtils {
|
||||
inline fun JSONArray.forEach(action: (JSONObject) -> Unit) {
|
||||
(0 until this.length()).forEach { i ->
|
||||
if (this[i] is JSONObject) {
|
||||
action(this[i] as JSONObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun JSONArray.forEachIndexed(action: (index: Int, JSONObject) -> Unit) {
|
||||
(0 until this.length()).forEach { i ->
|
||||
if (this[i] is JSONObject) {
|
||||
action(i, this[i] as JSONObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.links;
|
||||
|
||||
public final class ChangeLinkSharingDomainPatch {
|
||||
private static final String DOMAIN_NAME = "https://fxtwitter.com";
|
||||
private static final String LINK_FORMAT = "%s/%s/status/%s";
|
||||
|
||||
public static String formatResourceLink(Object... formatArgs) {
|
||||
String username = (String) formatArgs[0];
|
||||
String tweetId = (String) formatArgs[1];
|
||||
return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId);
|
||||
}
|
||||
|
||||
public static String formatLink(long tweetId, String username) {
|
||||
return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package app.revanced.extension.twitter.patches.links;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
public final class OpenLinksWithAppChooserPatch {
|
||||
public static void openWithChooser(final Context context, final Intent intent) {
|
||||
Log.d("ReVanced", "Opening intent with chooser: " + intent);
|
||||
|
||||
intent.setAction("android.intent.action.VIEW");
|
||||
|
||||
context.startActivity(Intent.createChooser(intent, null));
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package app.revanced.extension.twitter.utils.json
|
||||
|
||||
import app.revanced.extension.twitter.utils.stream.StreamUtils
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object JsonUtils {
|
||||
@JvmStatic
|
||||
@Throws(IOException::class, JSONException::class)
|
||||
fun parseJson(jsonInputStream: InputStream) = JSONObject(StreamUtils.toString(jsonInputStream))
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package app.revanced.extension.twitter.utils.stream
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
object StreamUtils {
|
||||
@Throws(IOException::class)
|
||||
fun toString(inputStream: InputStream): String {
|
||||
ByteArrayOutputStream().use { result ->
|
||||
val buffer = ByteArray(1024)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } != -1) {
|
||||
result.write(buffer, 0, length)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun fromString(string: String): InputStream {
|
||||
return ByteArrayInputStream(string.toByteArray())
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class ByteTrieSearch extends TrieSearch<byte[]> {
|
||||
|
||||
private static final class ByteTrieNode extends TrieNode<byte[]> {
|
||||
ByteTrieNode() {
|
||||
super();
|
||||
}
|
||||
ByteTrieNode(char nodeCharacterValue) {
|
||||
super(nodeCharacterValue);
|
||||
}
|
||||
@Override
|
||||
TrieNode<byte[]> createNode(char nodeCharacterValue) {
|
||||
return new ByteTrieNode(nodeCharacterValue);
|
||||
}
|
||||
@Override
|
||||
char getCharValue(byte[] text, int index) {
|
||||
return (char) text[index];
|
||||
}
|
||||
@Override
|
||||
int getTextLength(byte[] text) {
|
||||
return text.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for the common usage of converting Strings to raw UTF-8 bytes.
|
||||
*/
|
||||
public static byte[][] convertStringsToBytes(String... strings) {
|
||||
final int length = strings.length;
|
||||
byte[][] replacement = new byte[length][];
|
||||
for (int i = 0; i < length; i++) {
|
||||
replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
|
||||
public ByteTrieSearch(@NonNull byte[]... patterns) {
|
||||
super(new ByteTrieNode(), patterns);
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package app.revanced.extension.youtube
|
||||
|
||||
/**
|
||||
* generic event provider class
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
operator fun invoke(value: T) {
|
||||
for (observer in eventListeners)
|
||||
observer.invoke(value)
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Text pattern searching using a prefix tree (trie).
|
||||
*/
|
||||
public final class StringTrieSearch extends TrieSearch<String> {
|
||||
|
||||
private static final class StringTrieNode extends TrieNode<String> {
|
||||
StringTrieNode() {
|
||||
super();
|
||||
}
|
||||
StringTrieNode(char nodeCharacterValue) {
|
||||
super(nodeCharacterValue);
|
||||
}
|
||||
@Override
|
||||
TrieNode<String> createNode(char nodeValue) {
|
||||
return new StringTrieNode(nodeValue);
|
||||
}
|
||||
@Override
|
||||
char getCharValue(String text, int index) {
|
||||
return text.charAt(index);
|
||||
}
|
||||
@Override
|
||||
int getTextLength(String text) {
|
||||
return text.length();
|
||||
}
|
||||
}
|
||||
|
||||
public StringTrieSearch(@NonNull String... patterns) {
|
||||
super(new StringTrieNode(), patterns);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public class ThemeHelper {
|
||||
@Nullable
|
||||
private static Integer darkThemeColor, lightThemeColor;
|
||||
private static int themeValue;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setTheme(Enum<?> value) {
|
||||
final int newOrdinalValue = value.ordinal();
|
||||
if (themeValue != newOrdinalValue) {
|
||||
themeValue = newOrdinalValue;
|
||||
Logger.printDebug(() -> "Theme value: " + newOrdinalValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme() {
|
||||
return themeValue == 1;
|
||||
}
|
||||
|
||||
public static void setActivityTheme(Activity activity) {
|
||||
final var theme = isDarkTheme()
|
||||
? "Theme.YouTube.Settings.Dark"
|
||||
: "Theme.YouTube.Settings";
|
||||
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String darkThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_black3";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The dark theme color as specified by the Theme patch (if included),
|
||||
* or the dark mode background color unpatched YT uses.
|
||||
*/
|
||||
public static int getDarkThemeColor() {
|
||||
if (darkThemeColor == null) {
|
||||
darkThemeColor = getColorInt(darkThemeResourceName());
|
||||
}
|
||||
return darkThemeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String lightThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_white1";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The light theme color as specified by the Theme patch (if included),
|
||||
* or the non dark mode background color unpatched YT uses.
|
||||
*/
|
||||
public static int getLightThemeColor() {
|
||||
if (lightThemeColor == null) {
|
||||
lightThemeColor = getColorInt(lightThemeResourceName());
|
||||
}
|
||||
return lightThemeColor;
|
||||
}
|
||||
|
||||
private static int getColorInt(String colorString) {
|
||||
if (colorString.startsWith("#")) {
|
||||
return Color.parseColor(colorString);
|
||||
}
|
||||
return Utils.getResourceColor(colorString);
|
||||
}
|
||||
|
||||
public static int getBackgroundColor() {
|
||||
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
|
||||
}
|
||||
|
||||
public static int getForegroundColor() {
|
||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
||||
}
|
||||
}
|
@ -1,412 +0,0 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Searches for a group of different patterns using a trie (prefix tree).
|
||||
* Can significantly speed up searching for multiple patterns.
|
||||
*/
|
||||
public abstract class TrieSearch<T> {
|
||||
|
||||
public interface TriePatternMatchedCallback<T> {
|
||||
/**
|
||||
* Called when a pattern is matched.
|
||||
*
|
||||
* @param textSearched Text that was searched.
|
||||
* @param matchedStartIndex Start index of the search text, where the pattern was matched.
|
||||
* @param matchedLength Length of the match.
|
||||
* @param callbackParameter Optional parameter passed into {@link TrieSearch#matches(Object, Object)}.
|
||||
* @return True, if the search should stop here.
|
||||
* If false, searching will continue to look for other matches.
|
||||
*/
|
||||
boolean patternMatched(T textSearched, int matchedStartIndex, int matchedLength, Object callbackParameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a compressed tree path for a single pattern that shares no sibling nodes.
|
||||
*
|
||||
* For example, if a tree contains the patterns: "foobar", "football", "feet",
|
||||
* it would contain 3 compressed paths of: "bar", "tball", "eet".
|
||||
*
|
||||
* And the tree would contain children arrays only for the first level containing 'f',
|
||||
* the second level containing 'o',
|
||||
* and the third level containing 'o'.
|
||||
*
|
||||
* This is done to reduce memory usage, which can be substantial if many long patterns are used.
|
||||
*/
|
||||
private static final class TrieCompressedPath<T> {
|
||||
final T pattern;
|
||||
final int patternStartIndex;
|
||||
final int patternLength;
|
||||
final TriePatternMatchedCallback<T> callback;
|
||||
|
||||
TrieCompressedPath(T pattern, int patternStartIndex, int patternLength, TriePatternMatchedCallback<T> callback) {
|
||||
this.pattern = pattern;
|
||||
this.patternStartIndex = patternStartIndex;
|
||||
this.patternLength = patternLength;
|
||||
this.callback = callback;
|
||||
}
|
||||
boolean matches(TrieNode<T> enclosingNode, // Used only for the get character method.
|
||||
T searchText, int searchTextLength, int searchTextIndex, Object callbackParameter) {
|
||||
if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
|
||||
return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
|
||||
}
|
||||
for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
|
||||
if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return callback == null || callback.patternMatched(searchText,
|
||||
searchTextIndex - patternStartIndex, patternLength, callbackParameter);
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class TrieNode<T> {
|
||||
/**
|
||||
* Dummy value used for root node. Value can be anything as it's never referenced.
|
||||
*/
|
||||
private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character.
|
||||
|
||||
/**
|
||||
* How much to expand the children array when resizing.
|
||||
*/
|
||||
private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2;
|
||||
|
||||
/**
|
||||
* Character this node represents.
|
||||
* This field is ignored for the root node (which does not represent any character).
|
||||
*/
|
||||
private final char nodeValue;
|
||||
|
||||
/**
|
||||
* A compressed graph path that represents the remaining pattern characters of a single child node.
|
||||
*
|
||||
* If present then child array is always null, although callbacks for other
|
||||
* end of patterns can also exist on this same node.
|
||||
*/
|
||||
@Nullable
|
||||
private TrieCompressedPath<T> leaf;
|
||||
|
||||
/**
|
||||
* All child nodes. Only present if no compressed leaf exist.
|
||||
*
|
||||
* Array is dynamically increased in size as needed,
|
||||
* and uses perfect hashing for the elements it contains.
|
||||
*
|
||||
* So if the array contains a given character,
|
||||
* the character will always map to the node with index: (character % arraySize).
|
||||
*
|
||||
* Elements not contained can collide with elements the array does contain,
|
||||
* so must compare the nodes character value.
|
||||
*
|
||||
* Alternatively this array could be a sorted and densely packed array,
|
||||
* and lookup is done using binary search.
|
||||
* That would save a small amount of memory because there's no null children entries,
|
||||
* but would give a worst case search of O(nlog(m)) where n is the number of
|
||||
* characters in the searched text and m is the maximum size of the sorted character arrays.
|
||||
* Using a hash table array always gives O(n) search time.
|
||||
* The memory usage here is very small (all Litho filters use ~10KB of memory),
|
||||
* so the more performant hash implementation is chosen.
|
||||
*/
|
||||
@Nullable
|
||||
private TrieNode<T>[] children;
|
||||
|
||||
/**
|
||||
* Callbacks for all patterns that end at this node.
|
||||
*/
|
||||
@Nullable
|
||||
private List<TriePatternMatchedCallback<T>> endOfPatternCallback;
|
||||
|
||||
TrieNode() {
|
||||
this.nodeValue = ROOT_NODE_CHARACTER_VALUE;
|
||||
}
|
||||
TrieNode(char nodeCharacterValue) {
|
||||
this.nodeValue = nodeCharacterValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pattern Pattern to add.
|
||||
* @param patternIndex Current recursive index of the pattern.
|
||||
* @param patternLength Length of the pattern.
|
||||
* @param callback Callback, where a value of NULL indicates to always accept a pattern match.
|
||||
*/
|
||||
private void addPattern(@NonNull T pattern, int patternIndex, int patternLength,
|
||||
@Nullable TriePatternMatchedCallback<T> callback) {
|
||||
if (patternIndex == patternLength) { // Reached the end of the pattern.
|
||||
if (endOfPatternCallback == null) {
|
||||
endOfPatternCallback = new ArrayList<>(1);
|
||||
}
|
||||
endOfPatternCallback.add(callback);
|
||||
return;
|
||||
}
|
||||
if (leaf != null) {
|
||||
// Reached end of the graph and a leaf exist.
|
||||
// Recursively call back into this method and push the existing leaf down 1 level.
|
||||
if (children != null) throw new IllegalStateException();
|
||||
//noinspection unchecked
|
||||
children = new TrieNode[1];
|
||||
TrieCompressedPath<T> temp = leaf;
|
||||
leaf = null;
|
||||
addPattern(temp.pattern, temp.patternStartIndex, temp.patternLength, temp.callback);
|
||||
// Continue onward and add the parameter pattern.
|
||||
} else if (children == null) {
|
||||
leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
|
||||
return;
|
||||
}
|
||||
final char character = getCharValue(pattern, patternIndex);
|
||||
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
||||
TrieNode<T> child = children[arrayIndex];
|
||||
if (child == null) {
|
||||
child = createNode(character);
|
||||
children[arrayIndex] = child;
|
||||
} else if (child.nodeValue != character) {
|
||||
// Hash collision. Resize the table until perfect hashing is found.
|
||||
child = createNode(character);
|
||||
expandChildArray(child);
|
||||
}
|
||||
child.addPattern(pattern, patternIndex + 1, patternLength, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the children table until all nodes hash to exactly one array index.
|
||||
*/
|
||||
private void expandChildArray(TrieNode<T> child) {
|
||||
int replacementArraySize = Objects.requireNonNull(children).length;
|
||||
while (true) {
|
||||
replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT;
|
||||
//noinspection unchecked
|
||||
TrieNode<T>[] replacement = new TrieNode[replacementArraySize];
|
||||
addNodeToArray(replacement, child);
|
||||
boolean collision = false;
|
||||
for (TrieNode<T> existingChild : children) {
|
||||
if (existingChild != null) {
|
||||
if (!addNodeToArray(replacement, existingChild)) {
|
||||
collision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collision) {
|
||||
continue;
|
||||
}
|
||||
children = replacement;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> boolean addNodeToArray(TrieNode<T>[] array, TrieNode<T> childToAdd) {
|
||||
final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue);
|
||||
if (array[insertIndex] != null ) {
|
||||
return false; // Collision.
|
||||
}
|
||||
array[insertIndex] = childToAdd;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int hashIndexForTableSize(int arraySize, char nodeValue) {
|
||||
return nodeValue % arraySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is static and uses a loop to avoid all recursion.
|
||||
* This is done for performance since the JVM does not optimize tail recursion.
|
||||
*
|
||||
* @param startNode Node to start the search from.
|
||||
* @param searchText Text to search for patterns in.
|
||||
* @param searchTextIndex Start index, inclusive.
|
||||
* @param searchTextEndIndex End index, exclusive.
|
||||
* @return If any pattern matches, and it's associated callback halted the search.
|
||||
*/
|
||||
private static <T> boolean matches(final TrieNode<T> startNode, final T searchText,
|
||||
int searchTextIndex, final int searchTextEndIndex,
|
||||
final Object callbackParameter) {
|
||||
TrieNode<T> node = startNode;
|
||||
int currentMatchLength = 0;
|
||||
|
||||
while (true) {
|
||||
TrieCompressedPath<T> leaf = node.leaf;
|
||||
if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
|
||||
return true; // Leaf exists and it matched the search text.
|
||||
}
|
||||
List<TriePatternMatchedCallback<T>> endOfPatternCallback = node.endOfPatternCallback;
|
||||
if (endOfPatternCallback != null) {
|
||||
final int matchStartIndex = searchTextIndex - currentMatchLength;
|
||||
for (@Nullable TriePatternMatchedCallback<T> callback : endOfPatternCallback) {
|
||||
if (callback == null) {
|
||||
return true; // No callback and all matches are valid.
|
||||
}
|
||||
if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) {
|
||||
return true; // Callback confirmed the match.
|
||||
}
|
||||
}
|
||||
}
|
||||
TrieNode<T>[] children = node.children;
|
||||
if (children == null) {
|
||||
return false; // Reached a graph end point and there's no further patterns to search.
|
||||
}
|
||||
if (searchTextIndex == searchTextEndIndex) {
|
||||
return false; // Reached end of the search text and found no matches.
|
||||
}
|
||||
|
||||
// Use the start node to reduce VM method lookup, since all nodes are the same class type.
|
||||
final char character = startNode.getCharValue(searchText, searchTextIndex);
|
||||
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
||||
TrieNode<T> child = children[arrayIndex];
|
||||
if (child == null || child.nodeValue != character) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node = child;
|
||||
searchTextIndex++;
|
||||
currentMatchLength++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives an approximate memory usage.
|
||||
*
|
||||
* @return Estimated number of memory pointers used, starting from this node and including all children.
|
||||
*/
|
||||
private int estimatedNumberOfPointersUsed() {
|
||||
int numberOfPointers = 4; // Number of fields in this class.
|
||||
if (leaf != null) {
|
||||
numberOfPointers += 4; // Number of fields in leaf node.
|
||||
}
|
||||
if (endOfPatternCallback != null) {
|
||||
numberOfPointers += endOfPatternCallback.size();
|
||||
}
|
||||
if (children != null) {
|
||||
numberOfPointers += children.length;
|
||||
for (TrieNode<T> child : children) {
|
||||
if (child != null) {
|
||||
numberOfPointers += child.estimatedNumberOfPointersUsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
return numberOfPointers;
|
||||
}
|
||||
|
||||
abstract TrieNode<T> createNode(char nodeValue);
|
||||
abstract char getCharValue(T text, int index);
|
||||
abstract int getTextLength(T text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root node, and it's children represent the first pattern characters.
|
||||
*/
|
||||
private final TrieNode<T> root;
|
||||
|
||||
/**
|
||||
* Patterns to match.
|
||||
*/
|
||||
private final List<T> patterns = new ArrayList<>();
|
||||
|
||||
@SafeVarargs
|
||||
TrieSearch(@NonNull TrieNode<T> root, @NonNull T... patterns) {
|
||||
this.root = Objects.requireNonNull(root);
|
||||
addPatterns(patterns);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void addPatterns(@NonNull T... patterns) {
|
||||
for (T pattern : patterns) {
|
||||
addPattern(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a pattern that will always return a positive match if found.
|
||||
*
|
||||
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
||||
*/
|
||||
public void addPattern(@NonNull T pattern) {
|
||||
addPattern(pattern, root.getTextLength(pattern), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
|
||||
* @param callback Callback to determine if searching should halt when a match is found.
|
||||
*/
|
||||
public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback<T> callback) {
|
||||
addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
|
||||
if (patternLength == 0) return; // Nothing to match
|
||||
|
||||
patterns.add(pattern);
|
||||
root.addPattern(pattern, 0, patternLength, callback);
|
||||
}
|
||||
|
||||
public final boolean matches(@NonNull T textToSearch) {
|
||||
return matches(textToSearch, 0);
|
||||
}
|
||||
|
||||
public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) {
|
||||
return matches(textToSearch, 0, root.getTextLength(textToSearch),
|
||||
Objects.requireNonNull(callbackParameter));
|
||||
}
|
||||
|
||||
public boolean matches(@NonNull T textToSearch, int startIndex) {
|
||||
return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
|
||||
}
|
||||
|
||||
public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) {
|
||||
return matches(textToSearch, startIndex, endIndex, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches through text, looking for any substring that matches any pattern in this tree.
|
||||
*
|
||||
* @param textToSearch Text to search through.
|
||||
* @param startIndex Index to start searching, inclusive value.
|
||||
* @param endIndex Index to stop matching, exclusive value.
|
||||
* @param callbackParameter Optional parameter passed to the callbacks.
|
||||
* @return If any pattern matched, and it's callback halted searching.
|
||||
*/
|
||||
public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
|
||||
return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
|
||||
}
|
||||
|
||||
private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex,
|
||||
@Nullable Object callbackParameter) {
|
||||
if (endIndex > textToSearchLength) {
|
||||
throw new IllegalArgumentException("endIndex: " + endIndex
|
||||
+ " is greater than texToSearchLength: " + textToSearchLength);
|
||||
}
|
||||
if (patterns.isEmpty()) {
|
||||
return false; // No patterns were added.
|
||||
}
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
if (TrieNode.matches(root, textToSearch, i, endIndex, callbackParameter)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Estimated memory size (in kilobytes) of this instance.
|
||||
*/
|
||||
public int getEstimatedMemorySize() {
|
||||
if (patterns.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
// Assume the device has less than 32GB of ram (and can use pointer compression),
|
||||
// or the device is 32-bit.
|
||||
final int numberOfBytesPerPointer = 4;
|
||||
return (int) Math.ceil((numberOfBytesPerPointer * root.estimatedNumberOfPointersUsed()) / 1024.0);
|
||||
}
|
||||
|
||||
public int numberOfPatterns() {
|
||||
return patterns.size();
|
||||
}
|
||||
|
||||
public List<T> getPatterns() {
|
||||
return Collections.unmodifiableList(patterns);
|
||||
}
|
||||
}
|
@ -1,710 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.youtube.settings.Settings.*;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
import org.chromium.net.impl.CronetUrlRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* Alternative YouTube thumbnails.
|
||||
* <p>
|
||||
* Can show YouTube provided screen captures of beginning/middle/end of the video.
|
||||
* (ie: sd1.jpg, sd2.jpg, sd3.jpg).
|
||||
* <p>
|
||||
* Or can show crowd-sourced thumbnails provided by DeArrow (<a href="http://dearrow.ajay.app">...</a>).
|
||||
* <p>
|
||||
* Or can use DeArrow and fall back to screen captures if DeArrow is not available.
|
||||
* <p>
|
||||
* Has an additional option to use 'fast' video still thumbnails,
|
||||
* where it forces sd thumbnail quality and skips verifying if the alt thumbnail image exists.
|
||||
* The UI loading time will be the same or better than using original thumbnails,
|
||||
* but thumbnails will initially fail to load for all live streams, unreleased, and occasionally very old videos.
|
||||
* If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail
|
||||
* is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution,
|
||||
* because a noticeable number of videos do not have hq720 and too much fail to load.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class AlternativeThumbnailsPatch {
|
||||
|
||||
// These must be class declarations if declared here,
|
||||
// otherwise the app will not load due to cyclic initialization errors.
|
||||
public static final class DeArrowAvailability implements Setting.Availability {
|
||||
public static boolean usingDeArrowAnywhere() {
|
||||
return ALT_THUMBNAIL_HOME.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_LIBRARY.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_PLAYER.get().useDeArrow
|
||||
|| ALT_THUMBNAIL_SEARCH.get().useDeArrow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return usingDeArrowAnywhere();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StillImagesAvailability implements Setting.Availability {
|
||||
public static boolean usingStillImagesAnywhere() {
|
||||
return ALT_THUMBNAIL_HOME.get().useStillImages
|
||||
|| ALT_THUMBNAIL_SUBSCRIPTIONS.get().useStillImages
|
||||
|| ALT_THUMBNAIL_LIBRARY.get().useStillImages
|
||||
|| ALT_THUMBNAIL_PLAYER.get().useStillImages
|
||||
|| ALT_THUMBNAIL_SEARCH.get().useStillImages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return usingStillImagesAnywhere();
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThumbnailOption {
|
||||
ORIGINAL(false, false),
|
||||
DEARROW(true, false),
|
||||
DEARROW_STILL_IMAGES(true, true),
|
||||
STILL_IMAGES(false, true);
|
||||
|
||||
final boolean useDeArrow;
|
||||
final boolean useStillImages;
|
||||
|
||||
ThumbnailOption(boolean useDeArrow, boolean useStillImages) {
|
||||
this.useDeArrow = useDeArrow;
|
||||
this.useStillImages = useStillImages;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThumbnailStillTime {
|
||||
BEGINNING(1),
|
||||
MIDDLE(2),
|
||||
END(3);
|
||||
|
||||
/**
|
||||
* The url alt image number. Such as the 2 in 'hq720_2.jpg'
|
||||
*/
|
||||
final int altImageNumber;
|
||||
|
||||
ThumbnailStillTime(int altImageNumber) {
|
||||
this.altImageNumber = altImageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Uri dearrowApiUri;
|
||||
|
||||
/**
|
||||
* The scheme and host of {@link #dearrowApiUri}.
|
||||
*/
|
||||
private static final String deArrowApiUrlPrefix;
|
||||
|
||||
/**
|
||||
* How long to temporarily turn off DeArrow if it fails for any reason.
|
||||
*/
|
||||
private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
||||
|
||||
/**
|
||||
* If non zero, then the system time of when DeArrow API calls can resume.
|
||||
*/
|
||||
private static volatile long timeToResumeDeArrowAPICalls;
|
||||
|
||||
static {
|
||||
dearrowApiUri = validateSettings();
|
||||
final int port = dearrowApiUri.getPort();
|
||||
String portString = port == -1 ? "" : (":" + port);
|
||||
deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/";
|
||||
Logger.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix any bad imported data.
|
||||
*/
|
||||
private static Uri validateSettings() {
|
||||
Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
|
||||
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
||||
String scheme = apiUri.getScheme();
|
||||
if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) {
|
||||
Utils.showToastLong("Invalid DeArrow API URL. Using default");
|
||||
Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault();
|
||||
return validateSettings();
|
||||
}
|
||||
return apiUri;
|
||||
}
|
||||
|
||||
private static ThumbnailOption optionSettingForCurrentNavigation() {
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
return ALT_THUMBNAIL_PLAYER.get();
|
||||
}
|
||||
|
||||
// Must check second, as search can be from any tab.
|
||||
if (NavigationBar.isSearchBarActive()) {
|
||||
return ALT_THUMBNAIL_SEARCH.get();
|
||||
}
|
||||
|
||||
// Avoid checking which navigation button is selected, if all other settings are the same.
|
||||
ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get();
|
||||
ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get();
|
||||
ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get();
|
||||
if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) {
|
||||
return homeOption; // All are the same option.
|
||||
}
|
||||
|
||||
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
|
||||
if (selectedNavButton == null) {
|
||||
// Unknown tab, treat as the home tab;
|
||||
return homeOption;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.HOME) {
|
||||
return homeOption;
|
||||
}
|
||||
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) {
|
||||
return subscriptionsOption;
|
||||
}
|
||||
// A library tab variant is active.
|
||||
return libraryOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the alternative thumbnail url using YouTube provided still video captures.
|
||||
*
|
||||
* @param decodedUrl Decoded original thumbnail request url.
|
||||
* @return The alternative thumbnail url, or if not available NULL.
|
||||
*/
|
||||
@Nullable
|
||||
private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
|
||||
@NonNull ThumbnailQuality qualityToUse) {
|
||||
String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false);
|
||||
if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
|
||||
return sanitizedReplacement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the alternative thumbnail url using DeArrow thumbnail cache.
|
||||
*
|
||||
* @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short).
|
||||
* @param fallbackUrl URL to fall back to in case.
|
||||
* @return The alternative thumbnail url, without tracking parameters.
|
||||
*/
|
||||
@NonNull
|
||||
private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) {
|
||||
// Build thumbnail request url.
|
||||
// See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29.
|
||||
return dearrowApiUri
|
||||
.buildUpon()
|
||||
.appendQueryParameter("videoID", videoId)
|
||||
.appendQueryParameter("redirectUrl", fallbackUrl)
|
||||
.build()
|
||||
.toString();
|
||||
}
|
||||
|
||||
private static boolean urlIsDeArrow(@NonNull String imageUrl) {
|
||||
return imageUrl.startsWith(deArrowApiUrlPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If this client has not recently experienced any DeArrow API errors.
|
||||
*/
|
||||
private static boolean canUseDeArrowAPI() {
|
||||
if (timeToResumeDeArrowAPICalls == 0) {
|
||||
return true;
|
||||
}
|
||||
if (timeToResumeDeArrowAPICalls < System.currentTimeMillis()) {
|
||||
Logger.printDebug(() -> "Resuming DeArrow API calls");
|
||||
timeToResumeDeArrowAPICalls = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void handleDeArrowError(@NonNull String url, int statusCode) {
|
||||
Logger.printDebug(() -> "Encountered DeArrow error. Url: " + url);
|
||||
final long now = System.currentTimeMillis();
|
||||
if (timeToResumeDeArrowAPICalls < now) {
|
||||
timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS;
|
||||
if (Settings.ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST.get()) {
|
||||
String toastMessage = (statusCode != 0)
|
||||
? str("revanced_alt_thumbnail_dearrow_error", statusCode)
|
||||
: str("revanced_alt_thumbnail_dearrow_error_generic");
|
||||
Utils.showToastLong(toastMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||
*
|
||||
* @param originalUrl Image url for all url images loaded, including video thumbnails.
|
||||
*/
|
||||
public static String overrideImageURL(String originalUrl) {
|
||||
try {
|
||||
ThumbnailOption option = optionSettingForCurrentNavigation();
|
||||
|
||||
if (option == ThumbnailOption.ORIGINAL) {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl);
|
||||
if (decodedUrl == null) {
|
||||
return originalUrl; // Not a thumbnail.
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl);
|
||||
|
||||
ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality);
|
||||
if (qualityToUse == null) {
|
||||
// Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these).
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
String sanitizedReplacementUrl;
|
||||
final boolean includeTracking;
|
||||
if (option.useDeArrow && canUseDeArrowAPI()) {
|
||||
includeTracking = false; // Do not include view tracking parameters with API call.
|
||||
String fallbackUrl = null;
|
||||
if (option.useStillImages) {
|
||||
fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
}
|
||||
if (fallbackUrl == null) {
|
||||
fallbackUrl = decodedUrl.sanitizedUrl;
|
||||
}
|
||||
|
||||
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
||||
} else if (option.useStillImages) {
|
||||
includeTracking = true; // Include view tracking parameters if present.
|
||||
sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
|
||||
if (sanitizedReplacementUrl == null) {
|
||||
return originalUrl; // Still capture is not available. Return the untouched original url.
|
||||
}
|
||||
} else {
|
||||
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
|
||||
}
|
||||
|
||||
// Do not log any tracking parameters.
|
||||
Logger.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl);
|
||||
|
||||
return includeTracking
|
||||
? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters
|
||||
: sanitizedReplacementUrl;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "overrideImageURL failure", ex);
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Cronet considers all completed connections as a success, even if the response is 404 or 5xx.
|
||||
*/
|
||||
public static void handleCronetSuccess(UrlRequest request, @NonNull UrlResponseInfo responseInfo) {
|
||||
try {
|
||||
final int statusCode = responseInfo.getHttpStatusCode();
|
||||
if (statusCode == 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = responseInfo.getUrl();
|
||||
|
||||
if (urlIsDeArrow(url)) {
|
||||
Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode);
|
||||
if (statusCode == 304) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
|
||||
return; // Normal response.
|
||||
}
|
||||
handleDeArrowError(url, statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 404) {
|
||||
// Fast alt thumbnails is enabled and the thumbnail is not available.
|
||||
// The video is:
|
||||
// - live stream
|
||||
// - upcoming unreleased video
|
||||
// - very old
|
||||
// - very low view count
|
||||
// Take note of this, so if the image reloads the original thumbnail will be used.
|
||||
DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url);
|
||||
if (decodedUrl == null) {
|
||||
return; // Not a thumbnail.
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedUrl.sanitizedUrl);
|
||||
|
||||
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
|
||||
if (quality == null) {
|
||||
// Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen.
|
||||
Logger.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Callback success error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* To test failure cases, try changing the API URL to each of:
|
||||
* - A non-existent domain.
|
||||
* - A url path of something incorrect (ie: /v1/nonExistentEndPoint).
|
||||
* <p>
|
||||
* Cronet uses a very timeout (several minutes), so if the API never responds this hook can take a while to be called.
|
||||
* But this does not appear to be a problem, as the DeArrow API has not been observed to 'go silent'
|
||||
* Instead if there's a problem it returns an error code status response, which is handled in this patch.
|
||||
*/
|
||||
public static void handleCronetFailure(UrlRequest request,
|
||||
@Nullable UrlResponseInfo responseInfo,
|
||||
IOException exception) {
|
||||
try {
|
||||
String url = ((CronetUrlRequest) request).getHookedUrl();
|
||||
if (urlIsDeArrow(url)) {
|
||||
Logger.printDebug(() -> "handleCronetFailure, exception: " + exception);
|
||||
final int statusCode = (responseInfo != null)
|
||||
? responseInfo.getHttpStatusCode()
|
||||
: 0;
|
||||
handleDeArrowError(url, statusCode);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Callback failure error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private enum ThumbnailQuality {
|
||||
// In order of lowest to highest resolution.
|
||||
DEFAULT("default", ""), // effective alt name is 1.jpg, 2.jpg, 3.jpg
|
||||
MQDEFAULT("mqdefault", "mq"),
|
||||
HQDEFAULT("hqdefault", "hq"),
|
||||
SDDEFAULT("sddefault", "sd"),
|
||||
HQ720("hq720", "hq720_"),
|
||||
MAXRESDEFAULT("maxresdefault", "maxres");
|
||||
|
||||
/**
|
||||
* Lookup map of original name to enum.
|
||||
*/
|
||||
private static final Map<String, ThumbnailQuality> originalNameToEnum = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Lookup map of alt name to enum. ie: "hq720_1" to {@link #HQ720}.
|
||||
*/
|
||||
private static final Map<String, ThumbnailQuality> altNameToEnum = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (ThumbnailQuality quality : values()) {
|
||||
originalNameToEnum.put(quality.originalName, quality);
|
||||
|
||||
for (ThumbnailStillTime time : ThumbnailStillTime.values()) {
|
||||
// 'custom' thumbnails set by the content creator.
|
||||
// These show up in place of regular thumbnails
|
||||
// and seem to be limited to the same [1, 3] range as the still captures.
|
||||
originalNameToEnum.put(quality.originalName + "_custom_" + time.altImageNumber, quality);
|
||||
|
||||
altNameToEnum.put(quality.altImageName + time.altImageNumber, quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an alt image name to enum.
|
||||
* ie: "hq720_2" returns {@link #HQ720}.
|
||||
*/
|
||||
@Nullable
|
||||
static ThumbnailQuality altImageNameToQuality(@NonNull String altImageName) {
|
||||
return altNameToEnum.get(altImageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Original quality to effective alt quality to use.
|
||||
* ie: If fast alt image is enabled, then "hq720" returns {@link #SDDEFAULT}.
|
||||
*/
|
||||
@Nullable
|
||||
static ThumbnailQuality getQualityToUse(@NonNull String originalSize) {
|
||||
ThumbnailQuality quality = originalNameToEnum.get(originalSize);
|
||||
if (quality == null) {
|
||||
return null; // Not a thumbnail for a regular video.
|
||||
}
|
||||
|
||||
final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
|
||||
switch (quality) {
|
||||
case SDDEFAULT:
|
||||
// SD alt images have somewhat worse quality with washed out color and poor contrast.
|
||||
// But the 720 images look much better and don't suffer from these issues.
|
||||
// For unknown reasons, the 720 thumbnails are used only for the home feed,
|
||||
// while SD is used for the search and subscription feed
|
||||
// (even though search and subscriptions use the exact same layout as the home feed).
|
||||
// Of note, this image quality issue only appears with the alt thumbnail images,
|
||||
// and the regular thumbnails have identical color/contrast quality for all sizes.
|
||||
// Fix this by falling thru and upgrading SD to 720.
|
||||
case HQ720:
|
||||
if (useFastQuality) {
|
||||
return SDDEFAULT; // SD is max resolution for fast alt images.
|
||||
}
|
||||
return HQ720;
|
||||
case MAXRESDEFAULT:
|
||||
if (useFastQuality) {
|
||||
return SDDEFAULT;
|
||||
}
|
||||
return MAXRESDEFAULT;
|
||||
default:
|
||||
return quality;
|
||||
}
|
||||
}
|
||||
|
||||
final String originalName;
|
||||
final String altImageName;
|
||||
|
||||
ThumbnailQuality(String originalName, String altImageName) {
|
||||
this.originalName = originalName;
|
||||
this.altImageName = altImageName;
|
||||
}
|
||||
|
||||
String getAltImageNameToUse() {
|
||||
return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get().altImageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses HTTP HEAD requests to verify and keep track of which thumbnail sizes
|
||||
* are available and not available.
|
||||
*/
|
||||
private static class VerifiedQualities {
|
||||
/**
|
||||
* After a quality is verified as not available, how long until the quality is re-verified again.
|
||||
* Used only if fast mode is not enabled. Intended for live streams and unreleased videos
|
||||
* that are now finished and available (and thus, the alt thumbnails are also now available).
|
||||
*/
|
||||
private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes.
|
||||
|
||||
/**
|
||||
* Cache used to verify if an alternative thumbnails exists for a given video id.
|
||||
*/
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, VerifiedQualities> altVideoIdLookup = new LinkedHashMap<>(100) {
|
||||
private static final int CACHE_LIMIT = 1000;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Entry eldest) {
|
||||
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
|
||||
}
|
||||
};
|
||||
|
||||
private static VerifiedQualities getVerifiedQualities(@NonNull String videoId, boolean returnNullIfDoesNotExist) {
|
||||
synchronized (altVideoIdLookup) {
|
||||
VerifiedQualities verified = altVideoIdLookup.get(videoId);
|
||||
if (verified == null) {
|
||||
if (returnNullIfDoesNotExist) {
|
||||
return null;
|
||||
}
|
||||
verified = new VerifiedQualities();
|
||||
altVideoIdLookup.put(videoId, verified);
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||
@NonNull String imageUrl) {
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get());
|
||||
if (verified == null) return true; // Fast alt thumbnails is enabled.
|
||||
return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl);
|
||||
}
|
||||
|
||||
static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) {
|
||||
VerifiedQualities verified = getVerifiedQualities(videoId, false);
|
||||
//noinspection ConstantConditions
|
||||
verified.setQualityVerified(videoId, quality, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highest quality verified as existing.
|
||||
*/
|
||||
@Nullable
|
||||
private ThumbnailQuality highestQualityVerified;
|
||||
/**
|
||||
* Lowest quality verified as not existing.
|
||||
*/
|
||||
@Nullable
|
||||
private ThumbnailQuality lowestQualityNotAvailable;
|
||||
|
||||
/**
|
||||
* System time, of when to invalidate {@link #lowestQualityNotAvailable}.
|
||||
* Used only if fast mode is not enabled.
|
||||
*/
|
||||
private long timeToReVerifyLowestQuality;
|
||||
|
||||
private synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) {
|
||||
if (isVerified) {
|
||||
if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) {
|
||||
highestQualityVerified = quality;
|
||||
}
|
||||
} else {
|
||||
if (lowestQualityNotAvailable == null || lowestQualityNotAvailable.ordinal() > quality.ordinal()) {
|
||||
lowestQualityNotAvailable = quality;
|
||||
timeToReVerifyLowestQuality = System.currentTimeMillis() + NOT_AVAILABLE_TIMEOUT_MILLISECONDS;
|
||||
}
|
||||
Logger.printDebug(() -> quality + " not available for video: " + videoId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a video alt thumbnail exists. Does so by making a minimal HEAD http request.
|
||||
*/
|
||||
synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||
@NonNull String imageUrl) {
|
||||
if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) {
|
||||
return true; // Previously verified as existing.
|
||||
}
|
||||
|
||||
final boolean fastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
|
||||
if (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) {
|
||||
if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) {
|
||||
return false; // Previously verified as not existing.
|
||||
}
|
||||
// Enough time has passed, and should re-verify again.
|
||||
Logger.printDebug(() -> "Resetting lowest verified quality for: " + videoId);
|
||||
lowestQualityNotAvailable = null;
|
||||
}
|
||||
|
||||
if (fastQuality) {
|
||||
return true; // Unknown if it exists or not. Use the URL anyways and update afterwards if loading fails.
|
||||
}
|
||||
|
||||
boolean imageFileFound;
|
||||
try {
|
||||
// This hooked code is running on a low priority thread, and it's slightly faster
|
||||
// to run the url connection through the extension thread pool which runs at the highest priority.
|
||||
final long start = System.currentTimeMillis();
|
||||
imageFileFound = Utils.submitOnBackgroundThread(() -> {
|
||||
final int connectionTimeoutMillis = 10000; // 10 seconds.
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection();
|
||||
connection.setConnectTimeout(connectionTimeoutMillis);
|
||||
connection.setReadTimeout(connectionTimeoutMillis);
|
||||
connection.setRequestMethod("HEAD");
|
||||
// Even with a HEAD request, the response is the same size as a full GET request.
|
||||
// Using an empty range fixes this.
|
||||
connection.setRequestProperty("Range", "bytes=0-0");
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
||||
String contentType = connection.getContentType();
|
||||
return (contentType != null && contentType.startsWith("image"));
|
||||
}
|
||||
if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for url: " + imageUrl);
|
||||
}
|
||||
return false;
|
||||
}).get();
|
||||
Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl);
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex);
|
||||
imageFileFound = false;
|
||||
}
|
||||
|
||||
setQualityVerified(videoId, quality, imageFileFound);
|
||||
return imageFileFound;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube video thumbnail url, decoded into it's relevant parts.
|
||||
*/
|
||||
private static class DecodedThumbnailUrl {
|
||||
private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/";
|
||||
|
||||
@Nullable
|
||||
static DecodedThumbnailUrl decodeImageUrl(String url) {
|
||||
final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1;
|
||||
if (urlPathStartIndex <= 0) return null;
|
||||
|
||||
final int urlPathEndIndex = url.indexOf('/', urlPathStartIndex);
|
||||
if (urlPathEndIndex < 0) return null;
|
||||
|
||||
final int videoIdStartIndex = url.indexOf('/', urlPathEndIndex) + 1;
|
||||
if (videoIdStartIndex <= 0) return null;
|
||||
|
||||
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
|
||||
if (videoIdEndIndex < 0) return null;
|
||||
|
||||
final int imageSizeStartIndex = videoIdEndIndex + 1;
|
||||
final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex);
|
||||
if (imageSizeEndIndex < 0) return null;
|
||||
|
||||
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
|
||||
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
|
||||
|
||||
return new DecodedThumbnailUrl(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex,
|
||||
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
final String originalFullUrl;
|
||||
/** Full usable url, but stripped of any tracking information. */
|
||||
final String sanitizedUrl;
|
||||
/** Url path, such as 'vi' or 'vi_webp' */
|
||||
final String urlPath;
|
||||
final String videoId;
|
||||
/** Quality, such as hq720 or sddefault. */
|
||||
final String imageQuality;
|
||||
/** JPG or WEBP */
|
||||
final String imageExtension;
|
||||
/** User view tracking parameters, only present on some images. */
|
||||
final String viewTrackingParameters;
|
||||
|
||||
DecodedThumbnailUrl(String fullUrl, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex,
|
||||
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
|
||||
originalFullUrl = fullUrl;
|
||||
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
|
||||
urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex);
|
||||
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
|
||||
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
|
||||
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
|
||||
viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
|
||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
/** @noinspection SameParameterValue */
|
||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||
// especially for new videos uploaded in the last hour.
|
||||
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
|
||||
// (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file).
|
||||
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
|
||||
// Many different "i.ytimage.com" domains exist such as "i9.ytimg.com",
|
||||
// but still captures are frequently not available on the other domains (especially newly uploaded videos).
|
||||
// So always use the primary domain for a higher success rate.
|
||||
builder.append(YOUTUBE_THUMBNAIL_DOMAIN).append(urlPath).append('/');
|
||||
builder.append(videoId).append('/');
|
||||
builder.append(qualityToUse.getAltImageNameToUse());
|
||||
builder.append('.').append(imageExtension);
|
||||
if (includeViewTracking) {
|
||||
builder.append(viewTrackingParameters);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AutoRepeatPatch {
|
||||
//Used by app.revanced.patches.youtube.layout.autorepeat.patch.AutoRepeatPatch
|
||||
public static boolean shouldAutoRepeat() {
|
||||
return Settings.AUTO_REPEAT.get();
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BackgroundPlaybackPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isBackgroundPlaybackAllowed(boolean original) {
|
||||
if (original) return true;
|
||||
|
||||
// Steps to verify most edge cases (with Shorts background playback set to off):
|
||||
// 1. Open a regular video
|
||||
// 2. Minimize app (PIP should appear)
|
||||
// 3. Reopen app
|
||||
// 4. Open a Short (without closing the regular video)
|
||||
// (try opening both Shorts in the video player suggestions AND Shorts from the home feed)
|
||||
// 5. Minimize the app (PIP should not appear)
|
||||
// 6. Reopen app
|
||||
// 7. Close the Short
|
||||
// 8. Resume playing the regular video
|
||||
// 9. Minimize the app (PIP should appear)
|
||||
if (!VideoInformation.lastVideoIdIsShort()) {
|
||||
return true; // Definitely is not a Short.
|
||||
}
|
||||
|
||||
// TODO: Add better hook.
|
||||
// Might be a Shorts, or might be a prior regular video on screen again after a Shorts was closed.
|
||||
// This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Shorts,
|
||||
// But there's no way around this unless an additional hook is added to definitively detect
|
||||
// the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern.
|
||||
return !PlayerType.getCurrent().isNoneHiddenOrMinimized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isBackgroundShortsPlaybackAllowed(boolean original) {
|
||||
return !Settings.DISABLE_SHORTS_BACKGROUND_PLAYBACK.get();
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.youtube.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class BypassImageRegionRestrictionsPatch {
|
||||
|
||||
private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BYPASS_IMAGE_REGION_RESTRICTIONS.get();
|
||||
|
||||
private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com";
|
||||
|
||||
/**
|
||||
* YouTube static images domain. Includes user and channel avatar images and community post images.
|
||||
*/
|
||||
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
||||
= Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com");
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||
*
|
||||
* @param originalUrl Image url for all image urls loaded.
|
||||
*/
|
||||
public static String overrideImageURL(String originalUrl) {
|
||||
try {
|
||||
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
|
||||
String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
||||
.matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
|
||||
|
||||
if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) {
|
||||
Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'");
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "overrideImageURL failure", ex);
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user