mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-06 01:55:50 +01:00
chore: Merge branch dev
to main
(#577)
This commit is contained in:
commit
7760a39602
3
.editorconfig
Normal file
3
.editorconfig
Normal file
@ -0,0 +1,3 @@
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
109
CHANGELOG.md
109
CHANGELOG.md
@ -1,3 +1,112 @@
|
||||
# [1.5.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.9...v1.5.0-dev.10) (2024-03-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide Shorts components:** Selectively hide Shorts for home / subscription / search ([#592](https://github.com/ReVanced/revanced-integrations/issues/592)) ([1ee99aa](https://github.com/ReVanced/revanced-integrations/commit/1ee99aa6f0b4af15eeca25c7e21e8a0f5e9d189a))
|
||||
|
||||
# [1.5.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.8...v1.5.0-dev.9) (2024-03-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Check index of pattern in string instead of the other way around ([96a1e46](https://github.com/ReVanced/revanced-integrations/commit/96a1e4680d23be7154bb83290b1887fcd1a22f53))
|
||||
* **YouTube - Hide layout components:** Correctly hide Join button ([b945e2f](https://github.com/ReVanced/revanced-integrations/commit/b945e2f44b1a62326e6d45345c1467668d803f53))
|
||||
* **YouTube - Hide Shorts components:** Correctly hide join button ([b7a8995](https://github.com/ReVanced/revanced-integrations/commit/b7a8995f798e386ee1d9ab5bbd857c1736cc5a29))
|
||||
* **YouTube:** Fix video playback by switching to ReVanced GmsCore vendor ([#589](https://github.com/ReVanced/revanced-integrations/issues/589)) ([6e947e2](https://github.com/ReVanced/revanced-integrations/commit/6e947e24c2ac36e1bddcd25412870a36aa6c85c8))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide layout components:** Filter home/search results by keywords ([#584](https://github.com/ReVanced/revanced-integrations/issues/584)) ([0cbad98](https://github.com/ReVanced/revanced-integrations/commit/0cbad9820577c476f1f29b6ac77611b38afbb950))
|
||||
* **YouTube - Hide Shorts components:** Hide like and dislike buttons ([2a08e5a](https://github.com/ReVanced/revanced-integrations/commit/2a08e5a98e9e9a00bb306313ff487d67c042a92e))
|
||||
* **YouTube - Hide Shorts components:** Hide sound metadata label ([46d8ef6](https://github.com/ReVanced/revanced-integrations/commit/46d8ef6f88bd4c912a45357541291af38b5fc81f))
|
||||
* **YouTube - Hide Shorts components:** Hide title and full video link label ([59165de](https://github.com/ReVanced/revanced-integrations/commit/59165de801a5481fa4055dcf1797fe84dce235c0))
|
||||
|
||||
# [1.5.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.7...v1.5.0-dev.8) (2024-03-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide ads:** Prevent app crash if hiding fullscreen ads is not possible ([#590](https://github.com/ReVanced/revanced-integrations/issues/590)) ([4ec955f](https://github.com/ReVanced/revanced-integrations/commit/4ec955fd0133643826e47be7089fbfa07fd9a089))
|
||||
|
||||
# [1.5.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.6...v1.5.0-dev.7) (2024-03-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **TikTok:** Hook application context earlier to prevent crash ([#588](https://github.com/ReVanced/revanced-integrations/issues/588)) ([9ac2d63](https://github.com/ReVanced/revanced-integrations/commit/9ac2d634897d961eba1b704f2722ea757bb83e0a))
|
||||
|
||||
# [1.5.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.5...v1.5.0-dev.6) (2024-03-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Handle custom preferences ([#586](https://github.com/ReVanced/revanced-integrations/issues/586)) ([ad477e4](https://github.com/ReVanced/revanced-integrations/commit/ad477e4859ef69beda297f7a2a6c29a918077628))
|
||||
|
||||
# [1.5.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.4...v1.5.0-dev.5) (2024-03-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Downloads:** Use external downloader when selecting 'Download' in home feed flyout menu ([#587](https://github.com/ReVanced/revanced-integrations/issues/587)) ([254da31](https://github.com/ReVanced/revanced-integrations/commit/254da31d16c39781f46e1cdea1e9ba22e2fce6d1))
|
||||
|
||||
# [1.5.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.3...v1.5.0-dev.4) (2024-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - HDR auto brightness:** Remove non functional and obsolete patch ([#585](https://github.com/ReVanced/revanced-integrations/issues/585)) ([b060732](https://github.com/ReVanced/revanced-integrations/commit/b060732e861b011cac8737ed597385a3315f6057))
|
||||
|
||||
# [1.5.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.2...v1.5.0-dev.3) (2024-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Disable suggested video end screen:** Disable by default to fix autoplay issues ([#578](https://github.com/ReVanced/revanced-integrations/issues/578)) ([b9c1eec](https://github.com/ReVanced/revanced-integrations/commit/b9c1eec69fab64f213dd77d1f932e3244d81ab2d))
|
||||
|
||||
# [1.5.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.5.0-dev.1...v1.5.0-dev.2) (2024-03-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Downloads:** Use new task context ([#583](https://github.com/ReVanced/revanced-integrations/issues/583)) ([468dfac](https://github.com/ReVanced/revanced-integrations/commit/468dfac0544e282658675a8be65b4e43aa351068))
|
||||
|
||||
# [1.5.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.4.1-dev.4...v1.5.0-dev.1) (2024-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - External downloader:** Add ability to use in-app download button ([771a0de](https://github.com/ReVanced/revanced-integrations/commit/771a0de3bc9bec3ec5a8e4f8b02edfa9df7b1997))
|
||||
|
||||
## [1.4.1-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.4.1-dev.3...v1.4.1-dev.4) (2024-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide seekbar:** Use original seekbar color if Theme patch is not included ([#580](https://github.com/ReVanced/revanced-integrations/issues/580)) ([8d48a90](https://github.com/ReVanced/revanced-integrations/commit/8d48a90a8b8bc7ce9e22580b7a66c4c12fd6d54f))
|
||||
|
||||
## [1.4.1-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.4.1-dev.2...v1.4.1-dev.3) (2024-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert AGP dependency update ([ab07a56](https://github.com/ReVanced/revanced-integrations/commit/ab07a563b9ef890dc8a673eeb4268ce1c9f15a69))
|
||||
|
||||
## [1.4.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.4.1-dev.1...v1.4.1-dev.2) (2024-03-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Client spoof:** Allow playback for links with timestamp ([#582](https://github.com/ReVanced/revanced-integrations/issues/582)) ([eee3f35](https://github.com/ReVanced/revanced-integrations/commit/eee3f352c59141f47f6bda6c6cd350f1a16f1450))
|
||||
|
||||
## [1.4.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.4.0...v1.4.1-dev.1) (2024-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Announcements:** Only compare ID to not show same announcement after a fix-up ([#579](https://github.com/ReVanced/revanced-integrations/issues/579)) ([5d14f53](https://github.com/ReVanced/revanced-integrations/commit/5d14f53acd0b1eabd6951543edd7d7c662b6c502))
|
||||
|
||||
# [1.4.0](https://github.com/ReVanced/revanced-integrations/compare/v1.3.2...v1.4.0) (2024-03-02)
|
||||
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
package app.revanced.integrations.shared;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
@ -8,13 +6,11 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
@ -61,9 +57,8 @@ public class GmsCoreSupport {
|
||||
|
||||
private static String getGmsCoreDownloadLink() {
|
||||
final var vendor = getGmsCoreVendor();
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
switch (vendor) {
|
||||
case "com.mgoogle":
|
||||
return "https://github.com/TeamVanced/VancedMicroG/releases/latest";
|
||||
case "app.revanced":
|
||||
return "https://github.com/revanced/gmscore/releases/latest";
|
||||
default:
|
@ -5,14 +5,15 @@ import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_STACK
|
||||
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
|
||||
public class Logger {
|
||||
|
||||
/**
|
||||
@ -24,7 +25,7 @@ public class Logger {
|
||||
|
||||
/**
|
||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||
* For inner, static, or anonymous classes, this returns the simple name of the enclosing class.<br>
|
||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||
* <br>
|
||||
* For example, each of these classes return 'SomethingView':
|
||||
* <code>
|
||||
@ -38,13 +39,13 @@ public class Logger {
|
||||
|
||||
String fullClassName = selfClass.getName();
|
||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||
if (dollarSignIndex == -1) {
|
||||
return selfClass.getSimpleName(); // already an outer class
|
||||
if (dollarSignIndex < 0) {
|
||||
return selfClass.getSimpleName(); // Already an outer class.
|
||||
}
|
||||
|
||||
// class is inner, static, or anonymous
|
||||
// parse the simple name full name
|
||||
// a class with no package returns index of -1, but incrementing gives index zero which is correct
|
||||
// Class is inner, static, or anonymous.
|
||||
// Parse the simple name full name.
|
||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||
}
|
||||
@ -137,11 +138,19 @@ public class Logger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#context} may not be initialized.
|
||||
* Always logs even if Debugging is not enabled.
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||
* Normally this method should not be used.
|
||||
*/
|
||||
public static void initializationError(@NonNull Class<?> callingClass, @NonNull String message, @Nullable Exception ex) {
|
||||
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
|
||||
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||
* Normally this method should not be used.
|
||||
*/
|
||||
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
|
||||
@Nullable Exception ex) {
|
||||
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ import kotlin.text.Regex;
|
||||
public class Utils {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static Context context;
|
||||
private static Context context;
|
||||
|
||||
private static String versionName;
|
||||
|
||||
@ -54,13 +54,14 @@ public class Utils {
|
||||
try {
|
||||
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
||||
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
packageInfo = context.getPackageManager().getPackageInfo(
|
||||
packageInfo = packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(0)
|
||||
);
|
||||
else
|
||||
packageInfo = context.getPackageManager().getPackageInfo(
|
||||
packageInfo = packageManager.getPackageInfo(
|
||||
packageName,
|
||||
0
|
||||
);
|
||||
@ -195,18 +196,29 @@ public class Utils {
|
||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||
}
|
||||
|
||||
public interface MatchFilter<T> {
|
||||
boolean matches(T object);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchRecursively If children ViewGroups should also be
|
||||
* recursively searched using depth first search.
|
||||
* @return The first child view that matches the filter.
|
||||
*/
|
||||
@Nullable
|
||||
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, @NonNull MatchFilter filter) {
|
||||
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively,
|
||||
@NonNull MatchFilter<View> filter) {
|
||||
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
|
||||
View childAt = viewGroup.getChildAt(i);
|
||||
//noinspection unchecked
|
||||
if (filter.matches(childAt)) {
|
||||
//noinspection unchecked
|
||||
return (T) childAt;
|
||||
}
|
||||
// Must do recursive after filter check, in case the filter is looking for a ViewGroup.
|
||||
if (searchRecursively && childAt instanceof ViewGroup) {
|
||||
T match = getChildView((ViewGroup) childAt, true, filter);
|
||||
if (match != null) return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -222,17 +234,27 @@ public class Utils {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public interface MatchFilter<T> {
|
||||
boolean matches(T object);
|
||||
}
|
||||
|
||||
public static Context getContext() {
|
||||
if (context == null) {
|
||||
Logger.initializationError(Utils.class, "Context is null, returning null!", null);
|
||||
Logger.initializationException(Utils.class, "Context is null, returning null!", null);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
public static void setContext(Context appContext) {
|
||||
context = appContext;
|
||||
// In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies.
|
||||
// Calling the regular printDebug method here can cause a Settings context null pointer exception,
|
||||
// even though the context is already set before the call.
|
||||
//
|
||||
// The initialization logger methods do not directly or indirectly
|
||||
// reference the Context or any Settings and are unaffected by this problem.
|
||||
//
|
||||
// Info level also helps debug if a patch hook is called before
|
||||
// the context is set since debug logging is off by default.
|
||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
||||
}
|
||||
|
||||
public static void setClipboard(@NonNull String text) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
||||
@ -275,7 +297,7 @@ public class Utils {
|
||||
Objects.requireNonNull(messageToToast);
|
||||
runOnMainThreadNowOrLater(() -> {
|
||||
if (context == null) {
|
||||
Logger.initializationError(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
||||
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
Toast.makeText(context, messageToToast, toastDuration).show();
|
||||
|
@ -152,47 +152,60 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a UI Preference with the {@link Setting} that backs it.
|
||||
* Handles syncing a UI Preference with the {@link Setting} that backs it.
|
||||
* If needed, subclasses can override this to handle additional UI Preference types.
|
||||
*
|
||||
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
|
||||
* If false, then apply {@link Setting} <- Preference.
|
||||
*/
|
||||
protected void syncSettingWithPreference(@NonNull Preference pref,
|
||||
@NonNull Setting<?> setting,
|
||||
boolean applySettingToPreference) {
|
||||
if (pref instanceof SwitchPreference) {
|
||||
SwitchPreference switchPref = (SwitchPreference) pref;
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
if (applySettingToPreference) {
|
||||
switchPref.setChecked(boolSetting.get());
|
||||
} else {
|
||||
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
|
||||
}
|
||||
} else if (pref instanceof EditTextPreference) {
|
||||
EditTextPreference editPreference = (EditTextPreference) pref;
|
||||
if (applySettingToPreference) {
|
||||
editPreference.setText(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, editPreference.getText());
|
||||
}
|
||||
} else if (pref instanceof ListPreference) {
|
||||
ListPreference listPref = (ListPreference) pref;
|
||||
if (applySettingToPreference) {
|
||||
listPref.setValue(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, listPref.getValue());
|
||||
}
|
||||
updateListPreferenceSummary(listPref, setting);
|
||||
} else {
|
||||
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a UI Preference with the {@link Setting} that backs it.
|
||||
*
|
||||
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
|
||||
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
|
||||
* If false, then apply {@link Setting} <- Preference.
|
||||
*/
|
||||
protected void updatePreference(@NonNull Preference pref, @NonNull Setting<?> setting,
|
||||
boolean syncSetting, boolean applySettingToPreference) {
|
||||
private void updatePreference(@NonNull Preference pref, @NonNull Setting<?> setting,
|
||||
boolean syncSetting, boolean applySettingToPreference) {
|
||||
if (!syncSetting && applySettingToPreference) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
if (syncSetting) {
|
||||
if (pref instanceof SwitchPreference) {
|
||||
SwitchPreference switchPref = (SwitchPreference) pref;
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
if (applySettingToPreference) {
|
||||
switchPref.setChecked(boolSetting.get());
|
||||
} else {
|
||||
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
|
||||
}
|
||||
} else if (pref instanceof EditTextPreference) {
|
||||
EditTextPreference editPreference = (EditTextPreference) pref;
|
||||
if (applySettingToPreference) {
|
||||
editPreference.setText(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, editPreference.getText());
|
||||
}
|
||||
} else if (pref instanceof ListPreference) {
|
||||
ListPreference listPref = (ListPreference) pref;
|
||||
if (applySettingToPreference) {
|
||||
listPref.setValue(setting.get().toString());
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, listPref.getValue());
|
||||
}
|
||||
updateListPreferenceSummary(listPref, setting);
|
||||
} else {
|
||||
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
||||
return;
|
||||
}
|
||||
syncSettingWithPreference(pref, setting, applySettingToPreference);
|
||||
}
|
||||
|
||||
updatePreferenceAvailability(pref, setting);
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
package app.revanced.integrations.tiktok.settings.preference;
|
||||
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceScreen;
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.integrations.shared.settings.Setting;
|
||||
import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment;
|
||||
import app.revanced.integrations.tiktok.settings.preference.categories.DownloadsPreferenceCategory;
|
||||
import app.revanced.integrations.tiktok.settings.preference.categories.FeedFilterPreferenceCategory;
|
||||
import app.revanced.integrations.tiktok.settings.preference.categories.IntegrationsPreferenceCategory;
|
||||
import app.revanced.integrations.tiktok.settings.preference.categories.SimSpoofPreferenceCategory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Preference fragment for ReVanced settings
|
||||
@ -13,6 +17,21 @@ import app.revanced.integrations.tiktok.settings.preference.categories.SimSpoofP
|
||||
@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();
|
||||
|
@ -1,32 +1,37 @@
|
||||
package app.revanced.integrations.tiktok.spoof.sim;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.tiktok.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofSimPatch {
|
||||
public static boolean isEnable() {
|
||||
return Settings.SIM_SPOOF.get();
|
||||
}
|
||||
public static String getCountryIso(String value) {
|
||||
if (isEnable()) {
|
||||
return Settings.SIM_SPOOF_ISO.get();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
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 (isEnable()) {
|
||||
return Settings.SIMSPOOF_MCCMNC.get();
|
||||
} else {
|
||||
return 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 (isEnable()) {
|
||||
return Settings.SIMSPOOF_OP_NAME.get();
|
||||
} else {
|
||||
return value;
|
||||
if (ENABLED) {
|
||||
String operator = Settings.SIMSPOOF_OP_NAME.get();
|
||||
Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator);
|
||||
return operator;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,4 @@ abstract class BaseJsonHook : JsonHook {
|
||||
abstract fun apply(json: JSONObject)
|
||||
|
||||
override fun transform(json: JSONObject) = json.apply { apply(json) }
|
||||
}
|
||||
}
|
||||
|
@ -12,4 +12,4 @@ interface JsonHook : Hook<JSONObject> {
|
||||
fun transform(json: JSONObject): JSONObject
|
||||
|
||||
override fun hook(type: JSONObject) = transform(type)
|
||||
}
|
||||
}
|
||||
|
@ -27,4 +27,4 @@ object JsonHookPatch {
|
||||
|
||||
return StreamUtils.fromString(jsonObject.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,4 @@ interface Hook<T> {
|
||||
* @param type The type to hook
|
||||
*/
|
||||
fun hook(type: T): T
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import app.revanced.integrations.twitter.patches.hook.json.BaseJsonHook
|
||||
import app.revanced.integrations.twitter.patches.hook.twifucker.TwiFucker
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
object AdsHook : BaseJsonHook() {
|
||||
/**
|
||||
* Strips JSONObject from promoted ads.
|
||||
@ -12,4 +11,4 @@ object AdsHook : BaseJsonHook() {
|
||||
* @param json The JSONObject.
|
||||
*/
|
||||
override fun apply(json: JSONObject) = TwiFucker.hidePromotedAds(json)
|
||||
}
|
||||
}
|
||||
|
@ -11,4 +11,4 @@ object DummyHook : BaseJsonHook() {
|
||||
override fun apply(json: JSONObject) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import app.revanced.integrations.twitter.patches.hook.json.BaseJsonHook
|
||||
import app.revanced.integrations.twitter.patches.hook.twifucker.TwiFucker
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
object RecommendedUsersHook : BaseJsonHook() {
|
||||
/**
|
||||
* Strips JSONObject from recommended users.
|
||||
@ -12,4 +11,4 @@ object RecommendedUsersHook : BaseJsonHook() {
|
||||
* @param json The JSONObject.
|
||||
*/
|
||||
override fun apply(json: JSONObject) = TwiFucker.hideRecommendedUsers(json)
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,12 @@ package app.revanced.integrations.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));
|
||||
|
@ -10,4 +10,4 @@ object JsonUtils {
|
||||
@JvmStatic
|
||||
@Throws(IOException::class, JSONException::class)
|
||||
fun parseJson(jsonInputStream: InputStream) = JSONObject(StreamUtils.toString(jsonInputStream))
|
||||
}
|
||||
}
|
||||
|
@ -21,4 +21,4 @@ object StreamUtils {
|
||||
fun fromString(string: String): InputStream {
|
||||
return ByteArrayInputStream(string.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
package app.revanced.integrations.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[]> {
|
||||
@ -24,18 +28,18 @@ public final class ByteTrieSearch extends TrieSearch<byte[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the pattern is valid to add to this instance.
|
||||
* Helper method for the common usage of converting Strings to raw UTF-8 bytes.
|
||||
*/
|
||||
public static boolean isValidPattern(byte[] pattern) {
|
||||
for (byte b : pattern) {
|
||||
if (TrieNode.isInvalidRange((char) b)) {
|
||||
return false;
|
||||
}
|
||||
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 true;
|
||||
return replacement;
|
||||
}
|
||||
|
||||
public ByteTrieSearch() {
|
||||
super(new ByteTrieNode());
|
||||
public ByteTrieSearch(@NonNull byte[]... patterns) {
|
||||
super(new ByteTrieNode(), patterns);
|
||||
}
|
||||
}
|
||||
|
@ -27,4 +27,3 @@ class Event<T> {
|
||||
observer.invoke(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.integrations.youtube;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Text pattern searching using a prefix tree (trie).
|
||||
*/
|
||||
@ -26,19 +28,7 @@ public final class StringTrieSearch extends TrieSearch<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the pattern is valid to add to this instance.
|
||||
*/
|
||||
public static boolean isValidPattern(String pattern) {
|
||||
for (int i = 0, length = pattern.length(); i < length; i++) {
|
||||
if (TrieNode.isInvalidRange(pattern.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public StringTrieSearch() {
|
||||
super(new StringTrieNode());
|
||||
public StringTrieSearch(@NonNull String... patterns) {
|
||||
super(new StringTrieNode(), patterns);
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,6 @@ import java.util.Objects;
|
||||
/**
|
||||
* Searches for a group of different patterns using a trie (prefix tree).
|
||||
* Can significantly speed up searching for multiple patterns.
|
||||
*
|
||||
* Currently only supports ASCII non-control characters (letters/numbers/symbols).
|
||||
* But could be modified to also support UTF-8 unicode.
|
||||
*/
|
||||
public abstract class TrieSearch<T> {
|
||||
|
||||
@ -45,14 +42,14 @@ public abstract class TrieSearch<T> {
|
||||
*/
|
||||
private static final class TrieCompressedPath<T> {
|
||||
final T pattern;
|
||||
final int patternLength;
|
||||
final int patternStartIndex;
|
||||
final int patternLength;
|
||||
final TriePatternMatchedCallback<T> callback;
|
||||
|
||||
TrieCompressedPath(T pattern, int patternLength, int patternStartIndex, TriePatternMatchedCallback<T> callback) {
|
||||
TrieCompressedPath(T pattern, int patternStartIndex, int patternLength, TriePatternMatchedCallback<T> callback) {
|
||||
this.pattern = pattern;
|
||||
this.patternLength = patternLength;
|
||||
this.patternStartIndex = patternStartIndex;
|
||||
this.patternLength = patternLength;
|
||||
this.callback = callback;
|
||||
}
|
||||
boolean matches(TrieNode<T> enclosingNode, // Used only for the get character method.
|
||||
@ -76,19 +73,10 @@ public abstract class TrieSearch<T> {
|
||||
*/
|
||||
private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character.
|
||||
|
||||
// Support only ASCII letters/numbers/symbols and filter out all control characters.
|
||||
private static final char MIN_VALID_CHAR = 32; // Space character.
|
||||
private static final char MAX_VALID_CHAR = 126; // 127 = delete character.
|
||||
|
||||
/**
|
||||
* How much to expand the children array when resizing.
|
||||
*/
|
||||
private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2;
|
||||
private static final int CHILDREN_ARRAY_MAX_SIZE = MAX_VALID_CHAR - MIN_VALID_CHAR + 1;
|
||||
|
||||
static boolean isInvalidRange(char character) {
|
||||
return character < MIN_VALID_CHAR || character > MAX_VALID_CHAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Character this node represents.
|
||||
@ -144,11 +132,11 @@ public abstract class TrieSearch<T> {
|
||||
|
||||
/**
|
||||
* @param pattern Pattern to add.
|
||||
* @param patternLength Length of the pattern.
|
||||
* @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 patternLength, int patternIndex,
|
||||
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) {
|
||||
@ -165,16 +153,13 @@ public abstract class TrieSearch<T> {
|
||||
children = new TrieNode[1];
|
||||
TrieCompressedPath<T> temp = leaf;
|
||||
leaf = null;
|
||||
addPattern(temp.pattern, temp.patternLength, temp.patternStartIndex, temp.callback);
|
||||
addPattern(temp.pattern, temp.patternStartIndex, temp.patternLength, temp.callback);
|
||||
// Continue onward and add the parameter pattern.
|
||||
} else if (children == null) {
|
||||
leaf = new TrieCompressedPath<>(pattern, patternLength, patternIndex, callback);
|
||||
leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
|
||||
return;
|
||||
}
|
||||
final char character = getCharValue(pattern, patternIndex);
|
||||
if (isInvalidRange(character)) {
|
||||
throw new IllegalArgumentException("invalid character at index " + patternIndex + ": " + pattern);
|
||||
}
|
||||
final int arrayIndex = hashIndexForTableSize(children.length, character);
|
||||
TrieNode<T> child = children[arrayIndex];
|
||||
if (child == null) {
|
||||
@ -185,12 +170,11 @@ public abstract class TrieSearch<T> {
|
||||
child = createNode(character);
|
||||
expandChildArray(child);
|
||||
}
|
||||
child.addPattern(pattern, patternLength, patternIndex + 1, callback);
|
||||
child.addPattern(pattern, patternIndex + 1, patternLength, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the children table until all nodes hash to exactly one array index.
|
||||
* Worse case, this will resize the array to {@link #CHILDREN_ARRAY_MAX_SIZE} elements.
|
||||
*/
|
||||
private void expandChildArray(TrieNode<T> child) {
|
||||
int replacementArraySize = Objects.requireNonNull(children).length;
|
||||
@ -209,7 +193,6 @@ public abstract class TrieSearch<T> {
|
||||
}
|
||||
}
|
||||
if (collision) {
|
||||
if (replacementArraySize > CHILDREN_ARRAY_MAX_SIZE) throw new IllegalStateException();
|
||||
continue;
|
||||
}
|
||||
children = replacement;
|
||||
@ -232,22 +215,23 @@ public abstract class TrieSearch<T> {
|
||||
|
||||
/**
|
||||
* This method is static and uses a loop to avoid all recursion.
|
||||
* This is done for performance since the JVM does not do tail recursion optimization.
|
||||
* 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 searchTextLength Length of the search text.
|
||||
* @param searchTextIndex Current recursive search text index. Also, the end index of the current pattern match.
|
||||
* @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, final int searchTextLength,
|
||||
int searchTextIndex, final Object callbackParameter) {
|
||||
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(node, searchText, searchTextLength, searchTextIndex, callbackParameter)) {
|
||||
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;
|
||||
@ -266,7 +250,7 @@ public abstract class TrieSearch<T> {
|
||||
if (children == null) {
|
||||
return false; // Reached a graph end point and there's no further patterns to search.
|
||||
}
|
||||
if (searchTextIndex == searchTextLength) {
|
||||
if (searchTextIndex == searchTextEndIndex) {
|
||||
return false; // Reached end of the search text and found no matches.
|
||||
}
|
||||
|
||||
@ -323,8 +307,10 @@ public abstract class TrieSearch<T> {
|
||||
*/
|
||||
private final List<T> patterns = new ArrayList<>();
|
||||
|
||||
TrieSearch(@NonNull TrieNode<T> root) {
|
||||
@SafeVarargs
|
||||
TrieSearch(@NonNull TrieNode<T> root, @NonNull T... patterns) {
|
||||
this.root = Objects.requireNonNull(root);
|
||||
addPatterns(patterns);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@ -355,7 +341,7 @@ public abstract class TrieSearch<T> {
|
||||
if (patternLength == 0) return; // Nothing to match
|
||||
|
||||
patterns.add(pattern);
|
||||
root.addPattern(pattern, patternLength, 0, callback);
|
||||
root.addPattern(pattern, 0, patternLength, callback);
|
||||
}
|
||||
|
||||
public final boolean matches(@NonNull T textToSearch) {
|
||||
@ -398,7 +384,7 @@ public abstract class TrieSearch<T> {
|
||||
return false; // No patterns were added.
|
||||
}
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
if (TrieNode.matches(root, textToSearch, endIndex, i, callbackParameter)) return true;
|
||||
if (TrieNode.matches(root, textToSearch, i, endIndex, callbackParameter)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -0,0 +1,103 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.StringRef;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class DownloadsPatch {
|
||||
|
||||
private static WeakReference<Activity> activityRef = new WeakReference<>(null);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void activityCreated(Activity mainActivity) {
|
||||
activityRef = new WeakReference<>(mainActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called from the in app download hook,
|
||||
* for both the player action button (below the video)
|
||||
* and the 'Download video' flyout option for feed videos.
|
||||
*
|
||||
* Appears to always be called from the main thread.
|
||||
*/
|
||||
public static boolean inAppDownloadButtonOnClick(@NonNull String videoId) {
|
||||
try {
|
||||
if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If possible, use the main activity as the context.
|
||||
// Otherwise fall back on using the application context.
|
||||
Context context = activityRef.get();
|
||||
boolean isActivityContext = true;
|
||||
if (context == null) {
|
||||
// Utils context is the application context, and not an activity context.
|
||||
context = Utils.getContext();
|
||||
isActivityContext = false;
|
||||
}
|
||||
|
||||
launchExternalDownloader(videoId, context, isActivityContext);
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppDownloadButtonOnClick failure", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isActivityContext If the context parameter is for an Activity. If this is false, then
|
||||
* the downloader is opened as a new task (which forces YT to minimize).
|
||||
*/
|
||||
public static void launchExternalDownloader(@NonNull String videoId,
|
||||
@NonNull Context context, boolean isActivityContext) {
|
||||
try {
|
||||
Objects.requireNonNull(videoId);
|
||||
Logger.printDebug(() -> "Launching external downloader with context: " + context);
|
||||
|
||||
// Trim string to avoid any accidental whitespace.
|
||||
var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim();
|
||||
|
||||
boolean packageEnabled = false;
|
||||
try {
|
||||
packageEnabled = context.getPackageManager().getApplicationInfo(downloaderPackageName, 0).enabled;
|
||||
} catch (PackageManager.NameNotFoundException error) {
|
||||
Logger.printDebug(() -> "External downloader could not be found: " + error);
|
||||
}
|
||||
|
||||
// If the package is not installed, show the toast
|
||||
if (!packageEnabled) {
|
||||
Utils.showToastLong(StringRef.str("revanced_external_downloader_not_installed_warning", downloaderPackageName));
|
||||
return;
|
||||
}
|
||||
|
||||
String content = "https://youtu.be/" + videoId;
|
||||
Intent intent = new Intent("android.intent.action.SEND");
|
||||
intent.setType("text/plain");
|
||||
intent.setPackage(downloaderPackageName);
|
||||
intent.putExtra("android.intent.extra.TEXT", content);
|
||||
if (!isActivityContext) {
|
||||
Logger.printDebug(() -> "Using new task intent");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "launchExternalDownloader failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,8 +6,12 @@ import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity;
|
||||
|
||||
/**
|
||||
* Patch class for 'hdr-auto-brightness' patch
|
||||
* Patch class for 'hdr-auto-brightness' patch.
|
||||
*
|
||||
* Edit: This patch no longer does anything, as YT already uses BRIGHTNESS_OVERRIDE_NONE
|
||||
* as the default brightness level. The hooked code was also removed from YT 19.09+ as well.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("unused")
|
||||
public class HDRAutoBrightnessPatch {
|
||||
/**
|
||||
|
@ -16,7 +16,7 @@ public class HideBreakingNewsPatch {
|
||||
* Breaking news does not appear to be present in these older versions anyways.
|
||||
*/
|
||||
private static final boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory =
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("17.31.00");
|
||||
SpoofAppVersionPatch.isSpoofingToLessThan("18.01.00");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
|
@ -1,40 +1,41 @@
|
||||
package app.revanced.integrations.youtube.patches;
|
||||
|
||||
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class NavigationButtonsPatch {
|
||||
public static Enum lastNavigationButton;
|
||||
|
||||
public static void hideCreateButton(final View view) {
|
||||
view.setVisibility(Settings.HIDE_CREATE_BUTTON.get() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
private static final Map<NavigationButton, Boolean> shouldHideMap = new EnumMap<>(NavigationButton.class) {
|
||||
{
|
||||
put(NavigationButton.HOME, Settings.HIDE_HOME_BUTTON.get());
|
||||
put(NavigationButton.CREATE, Settings.HIDE_CREATE_BUTTON.get());
|
||||
put(NavigationButton.SHORTS, Settings.HIDE_SHORTS_BUTTON.get());
|
||||
}
|
||||
};
|
||||
|
||||
private static final Boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON
|
||||
= Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get();
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean switchCreateWithNotificationButton() {
|
||||
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get();
|
||||
return SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON;
|
||||
}
|
||||
|
||||
public static void hideButton(final View buttonView) {
|
||||
if (lastNavigationButton == null) return;
|
||||
|
||||
for (NavigationButton button : NavigationButton.values())
|
||||
if (button.name.equals(lastNavigationButton.name()))
|
||||
if (button.enabled) buttonView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private enum NavigationButton {
|
||||
HOME("PIVOT_HOME", Settings.HIDE_HOME_BUTTON.get()),
|
||||
SHORTS("TAB_SHORTS", Settings.HIDE_SHORTS_BUTTON.get()),
|
||||
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", Settings.HIDE_SUBSCRIPTIONS_BUTTON.get());
|
||||
private final boolean enabled;
|
||||
private final String name;
|
||||
|
||||
NavigationButton(final String name, final boolean enabled) {
|
||||
this.name = name;
|
||||
this.enabled = enabled;
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void navigationTabCreated(NavigationButton button, View tabView) {
|
||||
if (Boolean.TRUE.equals(shouldHideMap.get(button))) {
|
||||
tabView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,24 +6,12 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.youtube.shared.PlayerOverlays;
|
||||
|
||||
/**
|
||||
* Hook receiver class for 'player-overlays-hook' patch
|
||||
*
|
||||
* @usedBy app.revanced.patches.youtube.misc.playeroverlay.patch.PlayerOverlaysHookPatch
|
||||
* @smali Lapp/revanced/integrations/patches/PlayerOverlaysHookPatch;
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerOverlaysHookPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param thisRef reference to the view
|
||||
* @smali YouTubePlayerOverlaysLayout_onFinishInflateHook(Ljava / lang / Object ;)V
|
||||
*/
|
||||
public static void YouTubePlayerOverlaysLayout_onFinishInflateHook(@Nullable Object thisRef) {
|
||||
if (thisRef == null) return;
|
||||
if (thisRef instanceof ViewGroup) {
|
||||
PlayerOverlays.attach((ViewGroup) thisRef);
|
||||
}
|
||||
public static void playerOverlayInflated(ViewGroup group) {
|
||||
PlayerOverlays.attach(group);
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ import static app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTu
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER =
|
||||
SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.33.40");
|
||||
SpoofAppVersionPatch.isSpoofingToLessThan("18.34.00");
|
||||
|
||||
/**
|
||||
* RYD data for the current video on screen.
|
||||
|
@ -6,12 +6,11 @@ import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.patches.announcements.requests.AnnouncementsRoutes;
|
||||
import app.revanced.integrations.youtube.requests.Requester;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -19,7 +18,6 @@ import java.net.HttpURLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
@ -27,8 +25,6 @@ import static app.revanced.integrations.youtube.patches.announcements.requests.A
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class AnnouncementsPatch {
|
||||
private final static String CONSUMER = getOrSetConsumer();
|
||||
|
||||
private AnnouncementsPatch() {
|
||||
}
|
||||
|
||||
@ -42,16 +38,17 @@ public final class AnnouncementsPatch {
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(
|
||||
GET_LATEST_ANNOUNCEMENT, CONSUMER, Locale.getDefault().toLanguageTag());
|
||||
GET_LATEST_ANNOUNCEMENT, Locale.getDefault().toLanguageTag());
|
||||
|
||||
Logger.printDebug(() -> "Get latest announcement route connection url: " + connection.getURL());
|
||||
|
||||
try {
|
||||
// Do not show the announcement if the request failed.
|
||||
if (connection.getResponseCode() != 200) {
|
||||
if (Settings.ANNOUNCEMENT_LAST_HASH.get().isEmpty()) return;
|
||||
if (Settings.ANNOUNCEMENT_LAST_ID.isSetToDefault())
|
||||
return;
|
||||
|
||||
Settings.ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
Settings.ANNOUNCEMENT_LAST_ID.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_announcements_connection_failed"));
|
||||
|
||||
return;
|
||||
@ -65,22 +62,20 @@ public final class AnnouncementsPatch {
|
||||
|
||||
var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false);
|
||||
|
||||
// Do not show the announcement if it is older or the same as the last one.
|
||||
final byte[] hashBytes = MessageDigest.getInstance("SHA-256").digest(jsonString.getBytes(StandardCharsets.UTF_8));
|
||||
final var hash = java.util.Base64.getEncoder().encodeToString(hashBytes);
|
||||
if (hash.equals(Settings.ANNOUNCEMENT_LAST_HASH.get())) return;
|
||||
|
||||
// Parse the announcement. Fall-back to raw string if it fails.
|
||||
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||
String title;
|
||||
String message;
|
||||
Level level = Level.INFO;
|
||||
try {
|
||||
final var announcement = new JSONObject(jsonString);
|
||||
|
||||
id = announcement.getInt("id");
|
||||
title = announcement.getString("title");
|
||||
message = announcement.getJSONObject("content").getString("message");
|
||||
|
||||
if (!announcement.isNull("level")) level = Level.fromInt(announcement.getInt("level"));
|
||||
|
||||
} catch (Throwable ex) {
|
||||
Logger.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
|
||||
|
||||
@ -88,6 +83,28 @@ public final class AnnouncementsPatch {
|
||||
message = jsonString;
|
||||
}
|
||||
|
||||
// TODO: Remove this migration code after a few months.
|
||||
if (!Settings.DEPRECATED_ANNOUNCEMENT_LAST_HASH.isSetToDefault()){
|
||||
final byte[] hashBytes = MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest(jsonString.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
final var hash = java.util.Base64.getEncoder().encodeToString(hashBytes);
|
||||
|
||||
// Migrate to saving the id instead of the hash.
|
||||
if (hash.equals(Settings.DEPRECATED_ANNOUNCEMENT_LAST_HASH.get())) {
|
||||
Settings.ANNOUNCEMENT_LAST_ID.save(id);
|
||||
}
|
||||
|
||||
Settings.DEPRECATED_ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
}
|
||||
|
||||
// Do not show the announcement, if the last announcement id is the same as the current one.
|
||||
if (Settings.ANNOUNCEMENT_LAST_ID.get() == id) return;
|
||||
|
||||
|
||||
|
||||
int finalId = id;
|
||||
final var finalTitle = title;
|
||||
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
|
||||
final Level finalLevel = level;
|
||||
@ -99,7 +116,7 @@ public final class AnnouncementsPatch {
|
||||
.setMessage(finalMessage)
|
||||
.setIcon(finalLevel.icon)
|
||||
.setPositiveButton("Ok", (dialog, which) -> {
|
||||
Settings.ANNOUNCEMENT_LAST_HASH.save(hash);
|
||||
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton("Dismiss", (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
@ -119,27 +136,6 @@ public final class AnnouncementsPatch {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the last announcement hash if it is not empty.
|
||||
*
|
||||
* @return true if the last announcement hash was empty.
|
||||
*/
|
||||
private static boolean emptyLastAnnouncementHash() {
|
||||
if (Settings.ANNOUNCEMENT_LAST_HASH.get().isEmpty()) return true;
|
||||
Settings.ANNOUNCEMENT_LAST_HASH.resetToDefault();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getOrSetConsumer() {
|
||||
final var consumer = Settings.ANNOUNCEMENT_CONSUMER.get();
|
||||
if (!consumer.isEmpty()) return consumer;
|
||||
|
||||
final var uuid = UUID.randomUUID().toString();
|
||||
Settings.ANNOUNCEMENT_CONSUMER.save(uuid);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// TODO: Use better icons.
|
||||
private enum Level {
|
||||
INFO(android.R.drawable.ic_dialog_info),
|
||||
|
@ -14,7 +14,7 @@ public class AnnouncementsRoutes {
|
||||
/**
|
||||
* 'language' parameter is IETF format (for USA it would be 'en-us').
|
||||
*/
|
||||
public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?consumer={consumer}&language={language}");
|
||||
public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?language={language}");
|
||||
|
||||
private AnnouncementsRoutes() {
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
@ -170,7 +172,24 @@ public final class AdsFilter extends Filter {
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Must run off main thread (Odd, but whatever).
|
||||
Utils.runOnBackgroundThread(() -> instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK));
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
|
||||
} catch (Exception ex) {
|
||||
// Injecting user events on Android 10+ requires the manifest to include
|
||||
// INJECT_EVENTS, and it's usage is heavily restricted
|
||||
// and requires the user to manually approve the permission in the device settings.
|
||||
//
|
||||
// And no matter what, permissions cannot be added for root installations
|
||||
// as manifest changes are ignored for mount installations.
|
||||
//
|
||||
// Instead, catch the SecurityException and turn off hide full screen ads
|
||||
// since this functionality does not work for these devices.
|
||||
Logger.printInfo(() -> "Could not inject back button event", ex);
|
||||
Settings.HIDE_FULLSCREEN_ADS.save(false);
|
||||
Utils.showToastLong(str("revanced_hide_fullscreen_ads_feature_not_available_toast"));
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import java.util.regex.Pattern;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.ByteTrieSearch;
|
||||
import app.revanced.integrations.youtube.StringTrieSearch;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
@ -30,10 +29,6 @@ final class CustomFilter extends Filter {
|
||||
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
||||
}
|
||||
|
||||
private static void showInvalidCharactersToast(@NonNull String expression) {
|
||||
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_characters", expression));
|
||||
}
|
||||
|
||||
private static class CustomFilterGroup extends StringFilterGroup {
|
||||
/**
|
||||
* Optional character for the path that indicates the custom filter path must match the start.
|
||||
@ -73,7 +68,7 @@ final class CustomFilter extends Filter {
|
||||
Matcher matcher = pattern.matcher(expression);
|
||||
if (!matcher.find()) {
|
||||
showInvalidSyntaxToast(expression);
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String mapKey = matcher.group(1);
|
||||
@ -84,13 +79,7 @@ final class CustomFilter extends Filter {
|
||||
|
||||
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
|
||||
showInvalidSyntaxToast(expression);
|
||||
return null;
|
||||
}
|
||||
if (!StringTrieSearch.isValidPattern(path)
|
||||
|| (hasBufferSymbol && !StringTrieSearch.isValidPattern(bufferString))) {
|
||||
// Currently only ASCII is allowed.
|
||||
showInvalidCharactersToast(path);
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use one group object for all expressions with the same path.
|
||||
@ -149,11 +138,6 @@ final class CustomFilter extends Filter {
|
||||
|
||||
public CustomFilter() {
|
||||
Collection<CustomFilterGroup> groups = CustomFilterGroup.parseCustomFilterGroups();
|
||||
if (groups == null) {
|
||||
Settings.CUSTOM_FILTER_STRINGS.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_custom_filter_toast_reset"));
|
||||
groups = Objects.requireNonNull(CustomFilterGroup.parseCustomFilterGroups());
|
||||
}
|
||||
|
||||
if (!groups.isEmpty()) {
|
||||
CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
|
||||
|
@ -0,0 +1,280 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
import static app.revanced.integrations.youtube.ByteTrieSearch.convertStringsToBytes;
|
||||
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.ByteTrieSearch;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.shared.NavigationBar;
|
||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Allows hiding home feed and search results based on keywords and/or channel names.
|
||||
*
|
||||
* Limitations:
|
||||
* - Searching for a keyword phrase will give no search results.
|
||||
* This is because the buffer for each video contains the text the user searched for, and everything
|
||||
* will be filtered away (even if that video title/channel does not contain any keywords).
|
||||
* - Filtering a channel name can still show Shorts from that channel in the search results.
|
||||
* The most common Shorts layouts do not include the channel name, so they will not be filtered.
|
||||
* - Some layout component residue will remain, such as the video chapter previews for some search results.
|
||||
* These components do not include the video title or channel name, and they
|
||||
* appear outside the filtered components so they are not caught.
|
||||
* - Keywords are case sensitive, but some casing variation is manually added.
|
||||
* (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST").
|
||||
* - Keywords present in the layout or video data cannot be used as filters, otherwise all videos
|
||||
* will always be hidden. This patch checks for some words of these words.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
final class KeywordContentFilter extends Filter {
|
||||
|
||||
/**
|
||||
* Minimum keyword/phrase length to prevent excessively broad content filtering.
|
||||
*/
|
||||
private static final int MINIMUM_KEYWORD_LENGTH = 3;
|
||||
|
||||
/**
|
||||
* Strings found in the buffer for every videos.
|
||||
* Full strings should be specified, as they are compared using {@link String#contains(CharSequence)}.
|
||||
*
|
||||
* This list does not include every common buffer string, and this can be added/changed as needed.
|
||||
* Words must be entered with the exact casing as found in the buffer.
|
||||
*/
|
||||
private static final String[] STRINGS_IN_EVERY_BUFFER = {
|
||||
// Video playback data.
|
||||
"https://i.ytimg.com/vi/", // Thumbnail url.
|
||||
"sddefault.jpg", // More video sizes exist, but for most devices only these 2 are used.
|
||||
"hqdefault.webp",
|
||||
"googlevideo.com/initplayback?source=youtube", // Video url.
|
||||
"ANDROID", // Video url parameter.
|
||||
// Video decoders.
|
||||
"OMX.ffmpeg.vp9.decoder",
|
||||
"OMX.Intel.sw_vd.vp9",
|
||||
"OMX.sprd.av1.decoder",
|
||||
"OMX.MTK.VIDEO.DECODER.SW.VP9",
|
||||
"c2.android.av1.decoder",
|
||||
"c2.mtk.sw.vp9.decoder",
|
||||
// User analytics.
|
||||
"https://ad.doubleclick.net/ddm/activity/",
|
||||
"DEVICE_ADVERTISER_ID_FOR_CONVERSION_TRACKING",
|
||||
// Litho components frequently found in the buffer that belong to the path filter items.
|
||||
"metadata.eml",
|
||||
"thumbnail.eml",
|
||||
"avatar.eml",
|
||||
"overflow_button.eml",
|
||||
};
|
||||
|
||||
/**
|
||||
* Substrings that are always first in the identifier.
|
||||
*/
|
||||
private final StringFilterGroup startsWithFilter = new StringFilterGroup(
|
||||
null, // Multiple settings are used and must be individually checked if active.
|
||||
"home_video_with_context.eml",
|
||||
"search_video_with_context.eml",
|
||||
"video_with_context.eml", // Subscription tab videos.
|
||||
"related_video_with_context.eml",
|
||||
"compact_video.eml",
|
||||
"inline_shorts",
|
||||
"shorts_video_cell",
|
||||
"shorts_pivot_item.eml"
|
||||
);
|
||||
|
||||
/**
|
||||
* Substrings that are never at the start of the path.
|
||||
*/
|
||||
private final StringFilterGroup containsFilter = new StringFilterGroup(
|
||||
null,
|
||||
"modern_type_shelf_header_content.eml",
|
||||
"shorts_lockup_cell.eml" // Part of 'shorts_shelf_carousel.eml'
|
||||
);
|
||||
|
||||
/**
|
||||
* The last value of {@link Settings#HIDE_KEYWORD_CONTENT_PHRASES}
|
||||
* parsed and loaded into {@link #bufferSearch}.
|
||||
* Allows changing the keywords without restarting the app.
|
||||
*/
|
||||
private volatile String lastKeywordPhrasesParsed;
|
||||
|
||||
private volatile ByteTrieSearch bufferSearch;
|
||||
|
||||
private static void logNavigationState(String state) {
|
||||
// Enable locally to debug filtering. Default off to reduce log spam.
|
||||
final boolean LOG_NAVIGATION_STATE = false;
|
||||
// noinspection ConstantValue
|
||||
if (LOG_NAVIGATION_STATE) {
|
||||
Logger.printDebug(() -> "Navigation state: " + state);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hideKeywordSettingIsActive() {
|
||||
if (NavigationBar.isSearchBarActive()) {
|
||||
// Must check first. Search bar can be active with almost any tab.
|
||||
logNavigationState("Search");
|
||||
return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get();
|
||||
} else if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
// For now, consider the under video results the same as the home feed.
|
||||
logNavigationState("Player active");
|
||||
return Settings.HIDE_KEYWORD_CONTENT_HOME.get();
|
||||
} else if (NavigationButton.HOME.isSelected()) {
|
||||
logNavigationState("Home tab");
|
||||
return Settings.HIDE_KEYWORD_CONTENT_HOME.get();
|
||||
} else if (NavigationButton.SUBSCRIPTIONS.isSelected()) {
|
||||
logNavigationState("Subscription tab");
|
||||
return Settings.HIDE_SUBSCRIPTIONS_BUTTON.get();
|
||||
} else {
|
||||
// User is in the Library or Notifications tab.
|
||||
logNavigationState("Ignored tab");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change first letter of the first word to use title case.
|
||||
*/
|
||||
private static String titleCaseFirstWordOnly(String sentence) {
|
||||
if (sentence.isEmpty()) {
|
||||
return sentence;
|
||||
}
|
||||
final int firstCodePoint = sentence.codePointAt(0);
|
||||
// In some non English languages title case is different than upper case.
|
||||
return new StringBuilder()
|
||||
.appendCodePoint(Character.toTitleCase(firstCodePoint))
|
||||
.append(sentence, Character.charCount(firstCodePoint), sentence.length())
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uppercase the first letter of each word.
|
||||
*/
|
||||
private static String capitalizeAllFirstLetters(String sentence) {
|
||||
if (sentence.isEmpty()) {
|
||||
return sentence;
|
||||
}
|
||||
final int delimiter = ' ';
|
||||
// Use code points and not characters to handle unicode surrogates.
|
||||
int[] codePoints = sentence.codePoints().toArray();
|
||||
boolean capitalizeNext = true;
|
||||
for (int i = 0, length = codePoints.length; i < length; i++) {
|
||||
final int codePoint = codePoints[i];
|
||||
if (codePoint == delimiter) {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
codePoints[i] = Character.toUpperCase(codePoint);
|
||||
capitalizeNext = false;
|
||||
}
|
||||
}
|
||||
return new String(codePoints, 0, codePoints.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the phrase will will hide all videos. Not an exhaustive check.
|
||||
*/
|
||||
private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases) {
|
||||
for (String commonString : STRINGS_IN_EVERY_BUFFER) {
|
||||
if (Utils.containsAny(commonString, phrases)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded.
|
||||
String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get();
|
||||
if (rawKeywords == lastKeywordPhrasesParsed) {
|
||||
Logger.printDebug(() -> "Using previously initialized search");
|
||||
return; // Another thread won the race, and search is already initialized.
|
||||
}
|
||||
|
||||
ByteTrieSearch search = new ByteTrieSearch();
|
||||
String[] split = rawKeywords.split("\n");
|
||||
if (split.length != 0) {
|
||||
// Linked Set so log statement are more organized and easier to read.
|
||||
Set<String> keywords = new LinkedHashSet<>(10 * split.length);
|
||||
|
||||
for (String phrase : split) {
|
||||
// Remove any trailing white space the user may have accidentally included.
|
||||
phrase = phrase.stripTrailing();
|
||||
if (phrase.isBlank()) continue;
|
||||
|
||||
if (phrase.length() < MINIMUM_KEYWORD_LENGTH) {
|
||||
// Do not reset the setting. Keep the invalid keywords so the user can fix the mistake.
|
||||
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add common casing that might appear.
|
||||
//
|
||||
// This could be simplified by adding case insensitive search to the prefix search,
|
||||
// which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII.
|
||||
//
|
||||
// But to support Unicode with ByteTrieSearch would require major changes because
|
||||
// UTF-8 characters can be different byte lengths, which does
|
||||
// not allow comparing two different byte arrays using simple plain array indexes.
|
||||
//
|
||||
// Instead add all common case variations of the words.
|
||||
String[] phraseVariations = {
|
||||
phrase,
|
||||
phrase.toLowerCase(),
|
||||
titleCaseFirstWordOnly(phrase),
|
||||
capitalizeAllFirstLetters(phrase),
|
||||
phrase.toUpperCase()
|
||||
};
|
||||
if (phrasesWillHideAllVideos(phraseVariations)) {
|
||||
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_common", phrase));
|
||||
continue;
|
||||
}
|
||||
|
||||
keywords.addAll(Arrays.asList(phraseVariations));
|
||||
}
|
||||
|
||||
search.addPatterns(convertStringsToBytes(keywords.toArray(new String[0])));
|
||||
Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords);
|
||||
}
|
||||
|
||||
bufferSearch = search;
|
||||
lastKeywordPhrasesParsed = rawKeywords; // Must set last.
|
||||
}
|
||||
|
||||
public KeywordContentFilter() {
|
||||
// Keywords are parsed on first call to isFiltered()
|
||||
addPathCallbacks(startsWithFilter, containsFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hideKeywordSettingIsActive()) return false;
|
||||
|
||||
// Field is intentionally compared using reference equality.
|
||||
if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) {
|
||||
// User changed the keywords.
|
||||
parseKeywords();
|
||||
}
|
||||
|
||||
if (!bufferSearch.matches(protobufBufferArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
}
|
@ -29,6 +29,9 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
private final StringFilterGroup expandableMetadata;
|
||||
private final ByteArrayFilterGroup searchResultRecommendations;
|
||||
private final StringFilterGroup searchResultVideo;
|
||||
private final StringFilterGroup compactChannelBarInner;
|
||||
private final StringFilterGroup compactChannelBarInnerButton;
|
||||
private final ByteArrayFilterGroup joinMembershipButton;
|
||||
|
||||
static {
|
||||
mixPlaylistsExceptions.addPatterns(
|
||||
@ -37,6 +40,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
@ -194,9 +198,19 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"set_reminder_button"
|
||||
);
|
||||
|
||||
final var joinMembership = new StringFilterGroup(
|
||||
compactChannelBarInner = new StringFilterGroup(
|
||||
Settings.HIDE_JOIN_MEMBERSHIP_BUTTON,
|
||||
"compact_sponsor_button"
|
||||
"compact_channel_bar_inner"
|
||||
);
|
||||
|
||||
compactChannelBarInnerButton = new StringFilterGroup(
|
||||
null,
|
||||
"|button.eml|"
|
||||
);
|
||||
|
||||
joinMembershipButton = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"sponsorships"
|
||||
);
|
||||
|
||||
final var channelWatermark = new StringFilterGroup(
|
||||
@ -233,7 +247,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
quickActions,
|
||||
relatedVideos,
|
||||
compactBanner,
|
||||
joinMembership,
|
||||
compactChannelBarInner,
|
||||
medicalPanel,
|
||||
videoQualityMenuFooter,
|
||||
infoPanel,
|
||||
@ -265,6 +279,18 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
|
||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||
|
||||
if (matchedGroup == compactChannelBarInner) {
|
||||
if (compactChannelBarInnerButton.check(path).isFiltered()) {
|
||||
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
||||
// it's safe to assume that the button is the only thing that should be hidden.
|
||||
if (joinMembershipButton.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: This also hides the feed Shorts shelf header
|
||||
if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false;
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
@ -15,7 +14,6 @@ import java.util.Spliterator;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.shared.settings.BooleanSetting;
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
import app.revanced.integrations.youtube.ByteTrieSearch;
|
||||
@ -124,7 +122,7 @@ class StringFilterGroup extends FilterGroup<String> {
|
||||
if (isEnabled()) {
|
||||
for (String pattern : filters) {
|
||||
if (!string.isEmpty()) {
|
||||
final int indexOf = pattern.indexOf(string);
|
||||
final int indexOf = string.indexOf(pattern);
|
||||
if (indexOf >= 0) {
|
||||
matchedIndex = indexOf;
|
||||
matchedLength = pattern.length();
|
||||
@ -190,9 +188,8 @@ class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
/**
|
||||
* Converts the Strings into byte arrays. Used to search for text in binary data.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
|
||||
super(setting, Arrays.stream(filters).map(String::getBytes).toArray(byte[][]::new));
|
||||
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
|
||||
}
|
||||
|
||||
private synchronized void buildFailurePatterns() {
|
||||
|
@ -1,15 +1,19 @@
|
||||
package app.revanced.integrations.youtube.patches.components;
|
||||
|
||||
import static app.revanced.integrations.shared.Utils.hideViewUnderCondition;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import static app.revanced.integrations.shared.Utils.hideViewBy1dpUnderCondition;
|
||||
import static app.revanced.integrations.shared.Utils.hideViewUnderCondition;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.shared.NavigationBar;
|
||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@ -21,18 +25,24 @@ public final class ShortsFilter extends Filter {
|
||||
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
|
||||
|
||||
private final StringFilterGroup channelBar;
|
||||
private final StringFilterGroup fullVideoLinkLabel;
|
||||
private final StringFilterGroup videoTitle;
|
||||
private final StringFilterGroup reelSoundMetadata;
|
||||
private final StringFilterGroup subscribeButton;
|
||||
private final StringFilterGroup subscribeButtonPaused;
|
||||
private final StringFilterGroup soundButton;
|
||||
private final StringFilterGroup infoPanel;
|
||||
private final StringFilterGroup joinButton;
|
||||
private final StringFilterGroup shelfHeader;
|
||||
|
||||
private final StringFilterGroup videoActionButton;
|
||||
private final StringFilterGroup actionBar;
|
||||
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
public ShortsFilter() {
|
||||
// Identifier components.
|
||||
|
||||
var shorts = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS,
|
||||
null, // Setting is based on navigation state.
|
||||
"shorts_shelf",
|
||||
"inline_shorts",
|
||||
"shorts_grid",
|
||||
@ -42,7 +52,7 @@ public final class ShortsFilter extends Filter {
|
||||
// Feed Shorts shelf header.
|
||||
// Use a different filter group for this pattern, as it requires an additional check after matching.
|
||||
shelfHeader = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS,
|
||||
null,
|
||||
"shelf_header.eml"
|
||||
);
|
||||
|
||||
@ -54,17 +64,17 @@ public final class ShortsFilter extends Filter {
|
||||
|
||||
addIdentifierCallbacks(shorts, shelfHeader, thanksButton);
|
||||
|
||||
// Path components.
|
||||
|
||||
// Shorts that appear in the feed/search when the device is using tablet layout.
|
||||
shortsCompactFeedVideoPath = new StringFilterGroup(Settings.HIDE_SHORTS,
|
||||
"compact_video.eml");
|
||||
shortsCompactFeedVideoPath = new StringFilterGroup(null, "compact_video.eml");
|
||||
// Filter out items that use the 'frame0' thumbnail.
|
||||
// This is a valid thumbnail for both regular videos and Shorts,
|
||||
// but it appears these thumbnails are used only for Shorts.
|
||||
shortsCompactFeedVideoBuffer = new ByteArrayFilterGroup(Settings.HIDE_SHORTS,
|
||||
"/frame0.jpg");
|
||||
shortsCompactFeedVideoBuffer = new ByteArrayFilterGroup(null, "/frame0.jpg");
|
||||
|
||||
// Shorts player components.
|
||||
var joinButton = new StringFilterGroup(
|
||||
joinButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_JOIN_BUTTON,
|
||||
"sponsor_button"
|
||||
);
|
||||
@ -84,6 +94,21 @@ public final class ShortsFilter extends Filter {
|
||||
REEL_CHANNEL_BAR_PATH
|
||||
);
|
||||
|
||||
fullVideoLinkLabel = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_FULL_VIDEO_LINK_LABEL,
|
||||
"reel_multi_format_link"
|
||||
);
|
||||
|
||||
videoTitle = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_VIDEO_TITLE,
|
||||
"shorts_video_title_item"
|
||||
);
|
||||
|
||||
reelSoundMetadata = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_SOUND_METADATA_LABEL,
|
||||
"reel_sound_metadata"
|
||||
);
|
||||
|
||||
soundButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_SOUND_BUTTON,
|
||||
"reel_pivot_button"
|
||||
@ -94,15 +119,26 @@ public final class ShortsFilter extends Filter {
|
||||
"shorts_info_panel_overview"
|
||||
);
|
||||
|
||||
videoActionButton = new StringFilterGroup(
|
||||
actionBar = new StringFilterGroup(
|
||||
null,
|
||||
"ContainerType|shorts_video_action_button"
|
||||
"shorts_action_bar"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
shortsCompactFeedVideoPath,
|
||||
joinButton, subscribeButton, subscribeButtonPaused,
|
||||
channelBar, soundButton, infoPanel, videoActionButton
|
||||
channelBar, fullVideoLinkLabel, videoTitle, reelSoundMetadata,
|
||||
soundButton, infoPanel, actionBar
|
||||
);
|
||||
|
||||
var shortsLikeButton = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_LIKE_BUTTON,
|
||||
"shorts_like_button"
|
||||
);
|
||||
|
||||
var shortsDislikeButton = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
|
||||
"shorts_dislike_button"
|
||||
);
|
||||
|
||||
var shortsCommentButton = new ByteArrayFilterGroup(
|
||||
@ -120,7 +156,13 @@ public final class ShortsFilter extends Filter {
|
||||
"reel_remix_button"
|
||||
);
|
||||
|
||||
videoActionButtonGroupList.addAll(shortsCommentButton, shortsShareButton, shortsRemixButton);
|
||||
videoActionButtonGroupList.addAll(
|
||||
shortsLikeButton,
|
||||
shortsDislikeButton,
|
||||
shortsCommentButton,
|
||||
shortsShareButton,
|
||||
shortsRemixButton
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -131,18 +173,22 @@ public final class ShortsFilter extends Filter {
|
||||
if (matchedGroup == soundButton ||
|
||||
matchedGroup == infoPanel ||
|
||||
matchedGroup == channelBar ||
|
||||
matchedGroup == fullVideoLinkLabel ||
|
||||
matchedGroup == videoTitle ||
|
||||
matchedGroup == reelSoundMetadata ||
|
||||
matchedGroup == subscribeButtonPaused
|
||||
) return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
|
||||
if (matchedGroup == shortsCompactFeedVideoPath) {
|
||||
if (contentIndex == 0 && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
|
||||
if (shouldHideShortsFeedItems() && contentIndex == 0
|
||||
&& shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Video action buttons (comment, share, remix) have the same path.
|
||||
if (matchedGroup == videoActionButton) {
|
||||
// Video action buttons (like, dislike, comment, share, remix) have the same path.
|
||||
if (matchedGroup == actionBar) {
|
||||
if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
|
||||
);
|
||||
@ -151,25 +197,46 @@ public final class ShortsFilter extends Filter {
|
||||
|
||||
// Filter other path groups from pathFilterGroupList, only when reelChannelBar is visible
|
||||
// to avoid false positives.
|
||||
if (matchedGroup == subscribeButton) {
|
||||
if (matchedGroup == subscribeButton ||
|
||||
matchedGroup == joinButton
|
||||
) {
|
||||
if (path.startsWith(REEL_CHANNEL_BAR_PATH)) return super.isFiltered(
|
||||
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
|
||||
);
|
||||
); // else, return false.
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (matchedGroup == shelfHeader) {
|
||||
// Because the header is used in watch history and possibly other places, check for the index,
|
||||
// which is 0 when the shelf header is used for Shorts.
|
||||
if (contentIndex != 0) return false;
|
||||
} else {
|
||||
// Feed/search path components.
|
||||
if (matchedGroup == shelfHeader) {
|
||||
// Because the header is used in watch history and possibly other places, check for the index,
|
||||
// which is 0 when the shelf header is used for Shorts.
|
||||
if (contentIndex != 0) return false;
|
||||
}
|
||||
|
||||
if (!shouldHideShortsFeedItems()) return false;
|
||||
}
|
||||
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
||||
}
|
||||
|
||||
private static boolean shouldHideShortsFeedItems() {
|
||||
if (NavigationBar.isSearchBarActive()) { // Must check search first.
|
||||
return Settings.HIDE_SHORTS_SEARCH.get();
|
||||
} else if (PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
|| NavigationBar.NavigationButton.HOME.isSelected()) {
|
||||
return Settings.HIDE_SHORTS_HOME.get();
|
||||
} else if (NavigationBar.NavigationButton.SUBSCRIPTIONS.isSelected()) {
|
||||
return Settings.HIDE_SHORTS_SUBSCRIPTIONS.get();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void hideShortsShelf(final View shortsShelfView) {
|
||||
hideViewBy1dpUnderCondition(Settings.HIDE_SHORTS, shortsShelfView);
|
||||
if (shouldHideShortsFeedItems()) {
|
||||
Utils.hideViewByLayoutParams(shortsShelfView);
|
||||
}
|
||||
}
|
||||
|
||||
// region Hide the buttons in older versions of YouTube. New versions use Litho.
|
||||
|
@ -1,26 +1,12 @@
|
||||
package app.revanced.integrations.youtube.patches.spoof;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofAppVersionPatch {
|
||||
|
||||
private static final boolean SPOOF_APP_VERSION_ENABLED;
|
||||
private static final String SPOOF_APP_VERSION_TARGET;
|
||||
|
||||
static {
|
||||
// TODO: remove this migration code
|
||||
// Spoof targets below 17.33 that no longer reliably work.
|
||||
if (Settings.SPOOF_APP_VERSION_TARGET.get().compareTo("17.33.01") < 0) {
|
||||
Logger.printInfo(() -> "Resetting spoof app version target");
|
||||
Settings.SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||
}
|
||||
// End migration
|
||||
|
||||
SPOOF_APP_VERSION_ENABLED = Settings.SPOOF_APP_VERSION.get();
|
||||
SPOOF_APP_VERSION_TARGET = Settings.SPOOF_APP_VERSION_TARGET.get();
|
||||
}
|
||||
private static final boolean SPOOF_APP_VERSION_ENABLED = Settings.SPOOF_APP_VERSION.get();
|
||||
private static final String SPOOF_APP_VERSION_TARGET = Settings.SPOOF_APP_VERSION_TARGET.get();
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
@ -30,8 +16,8 @@ public class SpoofAppVersionPatch {
|
||||
return version;
|
||||
}
|
||||
|
||||
public static boolean isSpoofingToEqualOrLessThan(String version) {
|
||||
return SPOOF_APP_VERSION_ENABLED && SPOOF_APP_VERSION_TARGET.compareTo(version) <= 0;
|
||||
public static boolean isSpoofingToLessThan(String version) {
|
||||
return SPOOF_APP_VERSION_ENABLED && SPOOF_APP_VERSION_TARGET.compareTo(version) < 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,26 +1,25 @@
|
||||
package app.revanced.integrations.youtube.patches.spoof;
|
||||
|
||||
import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
||||
import static app.revanced.integrations.shared.Utils.containsAny;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.patches.VideoInformation;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.integrations.youtube.patches.VideoInformation;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.youtube.shared.PlayerType;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import static app.revanced.integrations.shared.Utils.containsAny;
|
||||
import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
||||
|
||||
/** @noinspection unused*/
|
||||
@Deprecated
|
||||
public class SpoofSignaturePatch {
|
||||
/**
|
||||
* Parameter (also used by
|
||||
@ -90,7 +89,7 @@ public class SpoofSignaturePatch {
|
||||
try {
|
||||
Logger.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||
|
||||
if (!Settings.SPOOF_SIGNATURE.get()) {
|
||||
if (parameters == null || !Settings.SPOOF_SIGNATURE.get()) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@ -98,7 +97,7 @@ public class SpoofSignaturePatch {
|
||||
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
||||
// Clips are 60 seconds or less in length, so no spoofing.
|
||||
//noinspection AssignmentUsedAsCondition
|
||||
if (useOriginalStoryboardRenderer = parameters.length() > 150 || containsAny(parameters, CLIPS_PARAMETERS)) {
|
||||
if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Deprecated
|
||||
public final class StoryboardRenderer {
|
||||
@Nullable
|
||||
private final String spec;
|
||||
|
@ -10,6 +10,7 @@ import org.json.JSONObject;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
@Deprecated
|
||||
final class PlayerRoutes {
|
||||
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
|
||||
static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
|
||||
|
@ -19,6 +19,7 @@ import java.util.Objects;
|
||||
import static app.revanced.integrations.shared.StringRef.str;
|
||||
import static app.revanced.integrations.youtube.patches.spoof.requests.PlayerRoutes.*;
|
||||
|
||||
@Deprecated
|
||||
public class StoryboardRendererRequester {
|
||||
|
||||
/**
|
||||
|
@ -91,7 +91,7 @@ public class ReturnYouTubeDislike {
|
||||
private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye'
|
||||
|
||||
private static final boolean IS_SPOOFING_TO_OLD_SEPARATOR_COLOR
|
||||
= SpoofAppVersionPatch.isSpoofingToEqualOrLessThan("18.09.39");
|
||||
= SpoofAppVersionPatch.isSpoofingToLessThan("18.10.00");
|
||||
|
||||
/**
|
||||
* Cached lookup of all video ids.
|
||||
|
@ -70,14 +70,16 @@ public class LicenseActivityHook {
|
||||
|
||||
private static void setToolbarTitle(Activity activity, String toolbarTitleResourceName) {
|
||||
ViewGroup toolbar = activity.findViewById(getToolbarResourceId());
|
||||
TextView toolbarTextView = Objects.requireNonNull(getChildView(toolbar, view -> view instanceof TextView));
|
||||
TextView toolbarTextView = Objects.requireNonNull(getChildView(toolbar, false,
|
||||
view -> view instanceof TextView));
|
||||
toolbarTextView.setText(getResourceIdentifier(toolbarTitleResourceName, "string"));
|
||||
}
|
||||
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
private static void setBackButton(Activity activity) {
|
||||
ViewGroup toolbar = activity.findViewById(getToolbarResourceId());
|
||||
ImageButton imageButton = Objects.requireNonNull(getChildView(toolbar, view -> view instanceof ImageButton));
|
||||
ImageButton imageButton = Objects.requireNonNull(getChildView(toolbar, false,
|
||||
view -> view instanceof ImageButton));
|
||||
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||
? "yt_outline_arrow_left_white_24"
|
||||
: "yt_outline_arrow_left_black_24",
|
||||
|
@ -1,42 +1,32 @@
|
||||
package app.revanced.integrations.youtube.settings;
|
||||
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.integrations.shared.settings.Setting.migrateFromOldPreferences;
|
||||
import static app.revanced.integrations.shared.settings.Setting.migrateOldSettingToNew;
|
||||
import static app.revanced.integrations.shared.settings.Setting.parent;
|
||||
import static app.revanced.integrations.shared.settings.Setting.parentsAny;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.IGNORE;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.settings.*;
|
||||
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import app.revanced.integrations.shared.settings.BaseSettings;
|
||||
import app.revanced.integrations.shared.settings.BooleanSetting;
|
||||
import app.revanced.integrations.shared.settings.FloatSetting;
|
||||
import app.revanced.integrations.shared.settings.IntegerSetting;
|
||||
import app.revanced.integrations.shared.settings.LongSetting;
|
||||
import app.revanced.integrations.shared.settings.Setting;
|
||||
import app.revanced.integrations.shared.settings.StringSetting;
|
||||
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
|
||||
import static app.revanced.integrations.shared.settings.Setting.*;
|
||||
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
|
||||
public class Settings extends BaseSettings {
|
||||
// External downloader
|
||||
public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE);
|
||||
public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action_button", FALSE);
|
||||
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_name",
|
||||
"org.schabi.newpipe" /* NewPipe */, parent(EXTERNAL_DOWNLOADER));
|
||||
"org.schabi.newpipe" /* NewPipe */, parentsAny(EXTERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER_ACTION_BUTTON));
|
||||
|
||||
// Copy video URL
|
||||
public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE);
|
||||
public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE);
|
||||
|
||||
// Video
|
||||
public static final BooleanSetting HDR_AUTO_BRIGHTNESS = new BooleanSetting("revanced_hdr_auto_brightness", TRUE);
|
||||
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
|
||||
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2);
|
||||
@ -45,6 +35,8 @@ public class Settings extends BaseSettings {
|
||||
public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", 1.0f);
|
||||
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds",
|
||||
"0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true);
|
||||
@Deprecated // Patch is obsolete and no longer works with 19.09+
|
||||
public static final BooleanSetting HDR_AUTO_BRIGHTNESS = new BooleanSetting("revanced_hdr_auto_brightness", TRUE);
|
||||
|
||||
// Ads
|
||||
public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE);
|
||||
@ -72,7 +64,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true);
|
||||
public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", FALSE);
|
||||
public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE);
|
||||
public static final BooleanSetting DISABLE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_disable_suggested_video_end_screen", TRUE);
|
||||
public static final BooleanSetting DISABLE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_disable_suggested_video_end_screen", FALSE);
|
||||
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE);
|
||||
public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_cards", FALSE, true);
|
||||
public static final BooleanSetting HIDE_ARTIST_CARDS = new BooleanSetting("revanced_hide_artist_cards", FALSE);
|
||||
@ -106,6 +98,11 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", TRUE);
|
||||
public static final BooleanSetting HIDE_JOIN_MEMBERSHIP_BUTTON = new BooleanSetting("revanced_hide_join_membership_button", TRUE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_HOME = new BooleanSetting("revanced_hide_keyword_content_home", FALSE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_keyword_content_subscriptions", FALSE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SEARCH = new BooleanSetting("revanced_hide_keyword_content_search", FALSE);
|
||||
public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "",
|
||||
parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_SEARCH));
|
||||
public static final BooleanSetting HIDE_LOAD_MORE_BUTTON = new BooleanSetting("revanced_hide_load_more_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_MEDICAL_PANELS = new BooleanSetting("revanced_hide_medical_panels", TRUE);
|
||||
public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", TRUE);
|
||||
@ -144,24 +141,32 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_TRANSCIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE);
|
||||
|
||||
// Shorts
|
||||
public static final BooleanSetting HIDE_SHORTS = new BooleanSetting("revanced_hide_shorts", FALSE, true);
|
||||
@Deprecated public static final BooleanSetting DEPRECATED_HIDE_SHORTS = new BooleanSetting("revanced_hide_shorts", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_HOME = new BooleanSetting("revanced_hide_shorts_home", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_shorts_subscriptions", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH = new BooleanSetting("revanced_hide_shorts_search", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_JOIN_BUTTON = new BooleanSetting("revanced_hide_shorts_join_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON = new BooleanSetting("revanced_hide_shorts_subscribe_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED = new BooleanSetting("revanced_hide_shorts_subscribe_button_paused", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_thanks_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_REMIX_BUTTON = new BooleanSetting("revanced_hide_shorts_remix_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHARE_BUTTON = new BooleanSetting("revanced_hide_shorts_share_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", TRUE, true);
|
||||
|
||||
// Seekbar
|
||||
public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE);
|
||||
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE);
|
||||
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE);
|
||||
public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", TRUE, true);
|
||||
public static final BooleanSetting SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true);
|
||||
public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0000", true, parent(SEEKBAR_CUSTOM_COLOR));
|
||||
|
||||
// Action buttons
|
||||
@ -204,8 +209,9 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
|
||||
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
|
||||
public static final StringSetting ANNOUNCEMENT_CONSUMER = new StringSetting("revanced_announcement_consumer", "", false, false);
|
||||
public static final StringSetting ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");
|
||||
@Deprecated
|
||||
public static final StringSetting DEPRECATED_ANNOUNCEMENT_LAST_HASH = new StringSetting("revanced_announcement_last_hash", "");
|
||||
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);
|
||||
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
|
||||
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG= new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
|
||||
"revanced_remove_viewer_discretion_dialog_user_dialog_message");
|
||||
@ -229,6 +235,10 @@ public class Settings extends BaseSettings {
|
||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||
|
||||
// Debugging
|
||||
/**
|
||||
* When enabled, share the debug logs with care.
|
||||
* The buffer contains select user data, including the client ip address and information that could identify the YT account.
|
||||
*/
|
||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG));
|
||||
|
||||
// ReturnYoutubeDislike
|
||||
@ -245,6 +255,7 @@ public class Settings extends BaseSettings {
|
||||
* Do not use directly, instead use {@link SponsorBlockSettings}
|
||||
*/
|
||||
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
|
||||
@Deprecated
|
||||
public static final StringSetting DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING = new StringSetting("uuid", ""); // Delete sometime in 2024
|
||||
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
||||
@ -343,6 +354,26 @@ public class Settings extends BaseSettings {
|
||||
// and more time should be given for users who rarely upgrade.
|
||||
migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID);
|
||||
|
||||
|
||||
// Old spoof versions that no longer work reliably.
|
||||
if (SpoofAppVersionPatch.isSpoofingToLessThan("17.33.00")) {
|
||||
Logger.printInfo(() -> "Resetting spoof app version target");
|
||||
Settings.SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||
}
|
||||
|
||||
|
||||
// Remove any previously saved announcement consumer (a random generated string).
|
||||
Setting.preferences.saveString("revanced_announcement_consumer", null);
|
||||
|
||||
// Shorts
|
||||
if (DEPRECATED_HIDE_SHORTS.get()) {
|
||||
Logger.printInfo(() -> "Migrating hide Shorts setting");
|
||||
DEPRECATED_HIDE_SHORTS.resetToDefault();
|
||||
HIDE_SHORTS_HOME.save(true);
|
||||
HIDE_SHORTS_SUBSCRIPTIONS.save(true);
|
||||
HIDE_SHORTS_SEARCH.save(true);
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
package app.revanced.integrations.youtube.settings.preference;
|
||||
|
||||
import android.os.Build;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceGroup;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment;
|
||||
import app.revanced.integrations.youtube.patches.DownloadsPatch;
|
||||
import app.revanced.integrations.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
@ -12,14 +19,20 @@ import app.revanced.integrations.youtube.settings.Settings;
|
||||
* @noinspection deprecation
|
||||
*/
|
||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
protected void initialize() {
|
||||
super.initialize();
|
||||
|
||||
// If the preference was included, then initialize it based on the available playback speed
|
||||
Preference defaultSpeedPreference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key);
|
||||
if (defaultSpeedPreference instanceof ListPreference) {
|
||||
CustomPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
|
||||
try {
|
||||
// If the preference was included, then initialize it based on the available playback speed.
|
||||
Preference defaultSpeedPreference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key);
|
||||
if (defaultSpeedPreference instanceof ListPreference) {
|
||||
CustomPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,184 @@
|
||||
package app.revanced.integrations.youtube.shared;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class NavigationBar {
|
||||
private static volatile boolean searchbarIsActive;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void searchBarResultsViewLoaded(View searchbarResults) {
|
||||
searchbarResults.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
final boolean isActive = searchbarResults.getParent() != null;
|
||||
|
||||
if (searchbarIsActive != isActive) {
|
||||
searchbarIsActive = isActive;
|
||||
Logger.printDebug(() -> "searchbarIsActive: " + isActive);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isSearchBarActive() {
|
||||
return searchbarIsActive;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Last YT navigation enum loaded. Not necessarily the active navigation tab.
|
||||
*/
|
||||
@Nullable
|
||||
private static volatile String lastYTNavigationEnumName;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) {
|
||||
if (ytNavigationEnumName != null) {
|
||||
lastYTNavigationEnumName = ytNavigationEnumName.name();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void navigationTabLoaded(final View navigationButtonGroup) {
|
||||
try {
|
||||
String lastEnumName = lastYTNavigationEnumName;
|
||||
for (NavigationButton button : NavigationButton.values()) {
|
||||
if (button.ytEnumName.equals(lastEnumName)) {
|
||||
ImageView imageView = Utils.getChildView((ViewGroup) navigationButtonGroup,
|
||||
true, view -> view instanceof ImageView);
|
||||
|
||||
if (imageView != null) {
|
||||
Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName);
|
||||
|
||||
button.imageViewRef = new WeakReference<>(imageView);
|
||||
navigationTabCreatedCallback(button, navigationButtonGroup);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Log the unknown tab as exception level, only if debug is enabled.
|
||||
// This is because unknown tabs do no harm, and it's only relevant to developers.
|
||||
if (Settings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Unknown tab: " + lastEnumName
|
||||
+ " view: " + navigationButtonGroup.getClass());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "navigationTabLoaded failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Unique hook just for the 'Create' and 'You' tab.
|
||||
*/
|
||||
public static void navigationImageResourceTabLoaded(View view) {
|
||||
// 'You' tab has no YT enum name and the enum hook is not called for it.
|
||||
// Compare the last enum to figure out which tab this actually is.
|
||||
if (CREATE.ytEnumName.equals(lastYTNavigationEnumName)) {
|
||||
navigationTabLoaded(view);
|
||||
} else {
|
||||
lastYTNavigationEnumName = NavigationButton.LIBRARY_YOU.ytEnumName;
|
||||
navigationTabLoaded(view);
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection EmptyMethod*/
|
||||
private static void navigationTabCreatedCallback(NavigationBar.NavigationButton button, View tabView) {
|
||||
// Code is added during patching.
|
||||
}
|
||||
|
||||
public enum NavigationButton {
|
||||
HOME("PIVOT_HOME"),
|
||||
SHORTS("TAB_SHORTS"),
|
||||
/**
|
||||
* Create new video tab.
|
||||
*
|
||||
* {@link #isSelected()} always returns false, even if the create video UI is on screen.
|
||||
*/
|
||||
CREATE("CREATION_TAB_LARGE"),
|
||||
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"),
|
||||
/**
|
||||
* Notifications tab. Only present when
|
||||
* {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active.
|
||||
*/
|
||||
ACTIVITY("TAB_ACTIVITY"),
|
||||
/**
|
||||
* Library tab when the user is not logged in.
|
||||
*/
|
||||
LIBRARY_LOGGED_OUT("ACCOUNT_CIRCLE"),
|
||||
/**
|
||||
* User is logged in with incognito mode enabled.
|
||||
*/
|
||||
LIBRARY_INCOGNITO("INCOGNITO_CIRCLE"),
|
||||
/**
|
||||
* Old library tab (pre 'You' layout), only present when version spoofing.
|
||||
*/
|
||||
LIBRARY_OLD_UI("VIDEO_LIBRARY_WHITE"),
|
||||
/**
|
||||
* 'You' library tab that is sometimes momentarily loaded.
|
||||
* When this is loaded, {@link #LIBRARY_YOU} is also present.
|
||||
*
|
||||
* This might be a temporary tab while the user profile photo is loading,
|
||||
* but its exact purpose is not entirely clear.
|
||||
*/
|
||||
LIBRARY_PIVOT_UNKNOWN("PIVOT_LIBRARY"),
|
||||
/**
|
||||
* Modern library tab with 'You' layout.
|
||||
*/
|
||||
// The hooked YT code does not use an enum, and a dummy name is used here.
|
||||
LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME");
|
||||
|
||||
/**
|
||||
* @return The active navigation tab.
|
||||
* If the user is in the create new video UI, this returns NULL.
|
||||
*/
|
||||
@Nullable
|
||||
public static NavigationButton getSelectedNavigationButton() {
|
||||
for (NavigationButton button : values()) {
|
||||
if (button.isSelected()) return button;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the currently selected tab is a 'You' or library type.
|
||||
* Covers all known app states including incognito mode and version spoofing.
|
||||
*/
|
||||
public static boolean libraryOrYouTabIsSelected() {
|
||||
return LIBRARY_YOU.isSelected() || LIBRARY_PIVOT_UNKNOWN.isSelected()
|
||||
|| LIBRARY_OLD_UI.isSelected() || LIBRARY_INCOGNITO.isSelected()
|
||||
|| LIBRARY_LOGGED_OUT.isSelected();
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube enum name for this tab.
|
||||
*/
|
||||
private final String ytEnumName;
|
||||
private volatile WeakReference<ImageView> imageViewRef = new WeakReference<>(null);
|
||||
|
||||
NavigationButton(String ytEnumName) {
|
||||
this.ytEnumName = ytEnumName;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
ImageView view = imageViewRef.get();
|
||||
return view != null && view.isSelected();
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import java.lang.ref.WeakReference
|
||||
* @param activity activity that contains the controls_layout view
|
||||
*/
|
||||
class PlayerControlsVisibilityObserverImpl(
|
||||
private val activity: Activity
|
||||
private val activity: Activity,
|
||||
) : PlayerControlsVisibilityObserver {
|
||||
|
||||
/**
|
||||
@ -81,4 +81,4 @@ interface PlayerControlsVisibilityObserver {
|
||||
* is the value of [playerControlsVisibility] equal to [View.VISIBLE]?
|
||||
*/
|
||||
val arePlayerControlsVisible: Boolean
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package app.revanced.integrations.youtube.shared
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.Rectangle
|
||||
import app.revanced.integrations.youtube.Event
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.Rectangle
|
||||
|
||||
/**
|
||||
* hooking class for player overlays
|
||||
@ -42,8 +42,8 @@ object PlayerOverlays {
|
||||
ChildrenChangeEventArgs(
|
||||
parent,
|
||||
child,
|
||||
false
|
||||
)
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -54,8 +54,8 @@ object PlayerOverlays {
|
||||
ChildrenChangeEventArgs(
|
||||
parent,
|
||||
child,
|
||||
true
|
||||
)
|
||||
true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -69,15 +69,15 @@ object PlayerOverlays {
|
||||
oldLeft,
|
||||
oldTop,
|
||||
oldRight - oldLeft,
|
||||
oldBottom - oldTop
|
||||
oldBottom - oldTop,
|
||||
),
|
||||
Rectangle(
|
||||
newLeft,
|
||||
newTop,
|
||||
newRight - newLeft,
|
||||
newBottom - newTop
|
||||
)
|
||||
)
|
||||
newBottom - newTop,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -87,11 +87,11 @@ object PlayerOverlays {
|
||||
data class ChildrenChangeEventArgs(
|
||||
val overlaysLayout: ViewGroup,
|
||||
val childView: View,
|
||||
val wasChildRemoved: Boolean
|
||||
val wasChildRemoved: Boolean,
|
||||
)
|
||||
|
||||
data class LayoutChangeEventArgs(
|
||||
val overlaysLayout: ViewGroup,
|
||||
val oldRect: Rectangle,
|
||||
val newRect: Rectangle
|
||||
val newRect: Rectangle,
|
||||
)
|
||||
|
@ -1,8 +1,8 @@
|
||||
package app.revanced.integrations.youtube.shared
|
||||
|
||||
import app.revanced.integrations.youtube.patches.VideoInformation
|
||||
import app.revanced.integrations.youtube.Event
|
||||
import app.revanced.integrations.shared.Logger
|
||||
import app.revanced.integrations.youtube.Event
|
||||
import app.revanced.integrations.youtube.patches.VideoInformation
|
||||
|
||||
/**
|
||||
* Main player type.
|
||||
@ -12,11 +12,13 @@ enum class PlayerType {
|
||||
* Either no video, or a Short is playing.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* A Short is playing. Occurs if a regular video is first opened
|
||||
* and then a Short is opened (without first closing the regular video).
|
||||
*/
|
||||
HIDDEN,
|
||||
|
||||
/**
|
||||
* A regular video is minimized.
|
||||
*
|
||||
@ -28,6 +30,7 @@ enum class PlayerType {
|
||||
WATCH_WHILE_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||
|
||||
/**
|
||||
* Player is either sliding to [HIDDEN] state because a Short was opened while a regular video is on screen.
|
||||
* OR
|
||||
@ -35,12 +38,14 @@ enum class PlayerType {
|
||||
*/
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||
|
||||
/**
|
||||
* Home feed video playback.
|
||||
*/
|
||||
INLINE_MINIMAL,
|
||||
VIRTUAL_REALITY_FULLSCREEN,
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE,
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@ -67,6 +72,7 @@ enum class PlayerType {
|
||||
currentPlayerType = value
|
||||
onChange(currentPlayerType)
|
||||
}
|
||||
|
||||
@Volatile // value is read/write from different threads
|
||||
private var currentPlayerType = NONE
|
||||
|
||||
@ -127,4 +133,7 @@ enum class PlayerType {
|
||||
return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED
|
||||
}
|
||||
|
||||
}
|
||||
fun isMaximizedOrFullscreen(): Boolean {
|
||||
return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,13 @@ enum class VideoState {
|
||||
PAUSED,
|
||||
RECOVERABLE_ERROR,
|
||||
UNRECOVERABLE_ERROR,
|
||||
|
||||
/**
|
||||
* @see [VideoInformation.isAtEndOfVideo]
|
||||
*/
|
||||
ENDED;
|
||||
ENDED,
|
||||
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@ -43,6 +46,6 @@ enum class VideoState {
|
||||
currentVideoState = value
|
||||
}
|
||||
|
||||
private var currentVideoState : VideoState? = null
|
||||
private var currentVideoState: VideoState? = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import app.revanced.integrations.youtube.shared.PlayerType
|
||||
* @param context the context to create in
|
||||
*/
|
||||
class SwipeControlsConfigurationProvider(
|
||||
private val context: Context
|
||||
private val context: Context,
|
||||
) {
|
||||
//region swipe enable
|
||||
/**
|
||||
@ -105,4 +105,4 @@ class SwipeControlsConfigurationProvider(
|
||||
get() = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get()
|
||||
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import app.revanced.integrations.shared.Logger.printDebug
|
||||
import app.revanced.integrations.shared.Logger.printException
|
||||
import app.revanced.integrations.youtube.shared.PlayerType
|
||||
import app.revanced.integrations.youtube.swipecontrols.controller.AudioVolumeController
|
||||
import app.revanced.integrations.youtube.swipecontrols.controller.ScreenBrightnessController
|
||||
@ -16,8 +18,6 @@ import app.revanced.integrations.youtube.swipecontrols.controller.gesture.PressT
|
||||
import app.revanced.integrations.youtube.swipecontrols.controller.gesture.core.GestureController
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.Rectangle
|
||||
import app.revanced.integrations.youtube.swipecontrols.views.SwipeControlsOverlayLayout
|
||||
import app.revanced.integrations.shared.Logger.printDebug
|
||||
import app.revanced.integrations.shared.Logger.printException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
@ -80,14 +80,18 @@ class SwipeControlsHostActivity : Activity() {
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
ensureInitialized()
|
||||
return if ((ev != null) && gesture.submitTouchEvent(ev)) true else {
|
||||
return if ((ev != null) && gesture.submitTouchEvent(ev)) {
|
||||
true
|
||||
} else {
|
||||
super.dispatchTouchEvent(ev)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
|
||||
ensureInitialized()
|
||||
return if ((ev != null) && keys.onKeyEvent(ev)) true else {
|
||||
return if ((ev != null) && keys.onKeyEvent(ev)) {
|
||||
true
|
||||
} else {
|
||||
super.dispatchKeyEvent(ev)
|
||||
}
|
||||
}
|
||||
@ -139,7 +143,7 @@ class SwipeControlsHostActivity : Activity() {
|
||||
contentRoot.x.toInt(),
|
||||
contentRoot.y.toInt(),
|
||||
contentRoot.width,
|
||||
contentRoot.height
|
||||
contentRoot.height,
|
||||
)
|
||||
}
|
||||
|
||||
@ -157,7 +161,7 @@ class SwipeControlsHostActivity : Activity() {
|
||||
* (re) attaches swipe overlays
|
||||
*/
|
||||
private fun reAttachOverlays() {
|
||||
printDebug{ "attaching swipe controls overlay" }
|
||||
printDebug { "attaching swipe controls overlay" }
|
||||
contentRoot.removeView(overlay)
|
||||
contentRoot.addView(overlay)
|
||||
}
|
||||
@ -168,7 +172,7 @@ class SwipeControlsHostActivity : Activity() {
|
||||
* @param type the new player type
|
||||
*/
|
||||
private fun onPlayerTypeChanged(type: PlayerType) {
|
||||
if (config.shouldSaveAndRestoreBrightness)
|
||||
if (config.shouldSaveAndRestoreBrightness) {
|
||||
when (type) {
|
||||
PlayerType.WATCH_WHILE_FULLSCREEN -> screen?.restore()
|
||||
else -> {
|
||||
@ -176,29 +180,38 @@ class SwipeControlsHostActivity : Activity() {
|
||||
screen?.restoreDefaultBrightness()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create the audio volume controller
|
||||
*/
|
||||
private fun createAudioController() =
|
||||
if (config.enableVolumeControls)
|
||||
AudioVolumeController(this) else null
|
||||
if (config.enableVolumeControls) {
|
||||
AudioVolumeController(this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* create the screen brightness controller instance
|
||||
*/
|
||||
private fun createScreenController() =
|
||||
if (config.enableBrightnessControl)
|
||||
ScreenBrightnessController(this) else null
|
||||
if (config.enableBrightnessControl) {
|
||||
ScreenBrightnessController(this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* create the gesture controller based on settings
|
||||
*/
|
||||
private fun createGestureController() =
|
||||
if (config.shouldEnablePressToSwipe)
|
||||
if (config.shouldEnablePressToSwipe) {
|
||||
PressToSwipeController(this)
|
||||
else ClassicSwipeController(this)
|
||||
} else {
|
||||
ClassicSwipeController(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -3,8 +3,8 @@ package app.revanced.integrations.youtube.swipecontrols.controller
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.clamp
|
||||
import app.revanced.integrations.shared.Logger.printException
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.clamp
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
@ -15,7 +15,7 @@ import kotlin.properties.Delegates
|
||||
*/
|
||||
class AudioVolumeController(
|
||||
context: Context,
|
||||
private val targetStream: Int = AudioManager.STREAM_MUSIC
|
||||
private val targetStream: Int = AudioManager.STREAM_MUSIC,
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -34,9 +34,13 @@ class AudioVolumeController(
|
||||
audioManager = mgr
|
||||
maximumVolumeIndex = audioManager.getStreamMaxVolume(targetStream)
|
||||
minimumVolumeIndex =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) audioManager.getStreamMinVolume(
|
||||
targetStream
|
||||
) else 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
audioManager.getStreamMinVolume(
|
||||
targetStream,
|
||||
)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import app.revanced.integrations.youtube.swipecontrols.misc.clamp
|
||||
* @param host the host activity of which the brightness is adjusted
|
||||
*/
|
||||
class ScreenBrightnessController(
|
||||
private val host: Activity
|
||||
private val host: Activity,
|
||||
) {
|
||||
/**
|
||||
* screen brightness saved by [save]
|
||||
@ -68,4 +68,4 @@ class ScreenBrightnessController(
|
||||
attr.screenBrightness = value
|
||||
host.window.attributes = attr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ package app.revanced.integrations.youtube.swipecontrols.controller
|
||||
import android.app.Activity
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import app.revanced.integrations.shared.Utils
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.Rectangle
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.applyDimension
|
||||
import app.revanced.integrations.shared.Utils
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
/**
|
||||
* Y- Axis:
|
||||
* -------- 0
|
||||
* ^
|
||||
@ -36,7 +36,7 @@ import kotlin.math.min
|
||||
@Suppress("PrivatePropertyName")
|
||||
class SwipeZonesController(
|
||||
private val host: Activity,
|
||||
private val fallbackScreenRect: () -> Rectangle
|
||||
private val fallbackScreenRect: () -> Rectangle,
|
||||
) {
|
||||
/**
|
||||
* 20dp, in pixels
|
||||
@ -74,7 +74,7 @@ class SwipeZonesController(
|
||||
p.x + _20dp,
|
||||
p.y + _40dp,
|
||||
p.width - _20dp,
|
||||
p.height - _20dp - _80dp
|
||||
p.height - _20dp - _80dp,
|
||||
)
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ class SwipeZonesController(
|
||||
eRect.right - zoneWidth,
|
||||
eRect.top,
|
||||
zoneWidth,
|
||||
eRect.height
|
||||
eRect.height,
|
||||
)
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ class SwipeZonesController(
|
||||
effectiveSwipeRect.left,
|
||||
effectiveSwipeRect.top,
|
||||
zoneWidth,
|
||||
effectiveSwipeRect.height
|
||||
effectiveSwipeRect.height,
|
||||
)
|
||||
}
|
||||
|
||||
@ -137,8 +137,8 @@ class SwipeZonesController(
|
||||
playerView.x.toInt(),
|
||||
playerView.y.toInt(),
|
||||
min(playerView.width, playerWidthWithPadding),
|
||||
playerView.height
|
||||
playerView.height,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity
|
||||
* @param controller main controller instance
|
||||
*/
|
||||
class VolumeKeysController(
|
||||
private val controller: SwipeControlsHostActivity
|
||||
private val controller: SwipeControlsHostActivity,
|
||||
) {
|
||||
/**
|
||||
* key event handler
|
||||
@ -18,7 +18,7 @@ class VolumeKeysController(
|
||||
* @return consume the event?
|
||||
*/
|
||||
fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
if(!controller.config.overwriteVolumeKeyControls) {
|
||||
if (!controller.config.overwriteVolumeKeyControls) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ import app.revanced.integrations.youtube.swipecontrols.misc.toPoint
|
||||
* @param controller reference to the main swipe controller
|
||||
*/
|
||||
class ClassicSwipeController(
|
||||
private val controller: SwipeControlsHostActivity
|
||||
private val controller: SwipeControlsHostActivity,
|
||||
) : BaseGestureController(controller),
|
||||
PlayerControlsVisibilityObserver by PlayerControlsVisibilityObserverImpl(controller) {
|
||||
/**
|
||||
@ -27,10 +27,16 @@ class ClassicSwipeController(
|
||||
get() = currentSwipe == SwipeDetector.SwipeDirection.VERTICAL
|
||||
|
||||
override fun isInSwipeZone(motionEvent: MotionEvent): Boolean {
|
||||
val inVolumeZone = if (controller.config.enableVolumeControls)
|
||||
(motionEvent.toPoint() in controller.zones.volume) else false
|
||||
val inBrightnessZone = if (controller.config.enableBrightnessControl)
|
||||
(motionEvent.toPoint() in controller.zones.brightness) else false
|
||||
val inVolumeZone = if (controller.config.enableVolumeControls) {
|
||||
(motionEvent.toPoint() in controller.zones.volume)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val inBrightnessZone = if (controller.config.enableBrightnessControl) {
|
||||
(motionEvent.toPoint() in controller.zones.brightness)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
return inVolumeZone || inBrightnessZone
|
||||
}
|
||||
@ -92,7 +98,7 @@ class ClassicSwipeController(
|
||||
from: MotionEvent,
|
||||
to: MotionEvent,
|
||||
distanceX: Double,
|
||||
distanceY: Double
|
||||
distanceY: Double,
|
||||
): Boolean {
|
||||
// cancel if not vertical
|
||||
if (currentSwipe != SwipeDetector.SwipeDirection.VERTICAL) return false
|
||||
|
@ -13,7 +13,7 @@ import app.revanced.integrations.youtube.swipecontrols.misc.toPoint
|
||||
* @param controller reference to the main swipe controller
|
||||
*/
|
||||
class PressToSwipeController(
|
||||
private val controller: SwipeControlsHostActivity
|
||||
private val controller: SwipeControlsHostActivity,
|
||||
) : BaseGestureController(controller) {
|
||||
/**
|
||||
* monitors if the user is currently in a swipe session.
|
||||
@ -26,10 +26,16 @@ class PressToSwipeController(
|
||||
override fun shouldDropMotion(motionEvent: MotionEvent): Boolean = false
|
||||
|
||||
override fun isInSwipeZone(motionEvent: MotionEvent): Boolean {
|
||||
val inVolumeZone = if (controller.config.enableVolumeControls)
|
||||
(motionEvent.toPoint() in controller.zones.volume) else false
|
||||
val inBrightnessZone = if (controller.config.enableBrightnessControl)
|
||||
(motionEvent.toPoint() in controller.zones.brightness) else false
|
||||
val inVolumeZone = if (controller.config.enableVolumeControls) {
|
||||
(motionEvent.toPoint() in controller.zones.volume)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val inBrightnessZone = if (controller.config.enableBrightnessControl) {
|
||||
(motionEvent.toPoint() in controller.zones.brightness)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
return inVolumeZone || inBrightnessZone
|
||||
}
|
||||
@ -53,7 +59,7 @@ class PressToSwipeController(
|
||||
from: MotionEvent,
|
||||
to: MotionEvent,
|
||||
distanceX: Double,
|
||||
distanceY: Double
|
||||
distanceY: Double,
|
||||
): Boolean {
|
||||
// cancel if not in swipe session or vertical
|
||||
if (!isInSwipeSession || currentSwipe != SwipeDetector.SwipeDirection.VERTICAL) return false
|
||||
|
@ -11,11 +11,11 @@ import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity
|
||||
* @param controller reference to the main swipe controller
|
||||
*/
|
||||
abstract class BaseGestureController(
|
||||
private val controller: SwipeControlsHostActivity
|
||||
private val controller: SwipeControlsHostActivity,
|
||||
) : GestureController,
|
||||
GestureDetector.SimpleOnGestureListener(),
|
||||
SwipeDetector by SwipeDetectorImpl(
|
||||
controller.config.swipeMagnitudeThreshold.toDouble()
|
||||
controller.config.swipeMagnitudeThreshold.toDouble(),
|
||||
),
|
||||
VolumeAndBrightnessScroller by VolumeAndBrightnessScrollerImpl(
|
||||
controller,
|
||||
@ -23,7 +23,7 @@ abstract class BaseGestureController(
|
||||
controller.screen,
|
||||
controller.overlay,
|
||||
10,
|
||||
1
|
||||
1,
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -85,7 +85,7 @@ abstract class BaseGestureController(
|
||||
from: MotionEvent,
|
||||
to: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
distanceY: Float,
|
||||
): Boolean {
|
||||
// submit to swipe detector
|
||||
submitForSwipe(from, to, distanceX, distanceY)
|
||||
@ -96,7 +96,7 @@ abstract class BaseGestureController(
|
||||
from,
|
||||
to,
|
||||
distanceX.toDouble(),
|
||||
distanceY.toDouble()
|
||||
distanceY.toDouble(),
|
||||
)
|
||||
|
||||
// if the swipe was consumed, cancel downstream events once
|
||||
@ -110,7 +110,9 @@ abstract class BaseGestureController(
|
||||
}
|
||||
|
||||
consumed
|
||||
} else false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,6 +151,6 @@ abstract class BaseGestureController(
|
||||
from: MotionEvent,
|
||||
to: MotionEvent,
|
||||
distanceX: Double,
|
||||
distanceY: Double
|
||||
distanceY: Double,
|
||||
): Boolean
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ interface SwipeDetector {
|
||||
from: MotionEvent,
|
||||
to: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
distanceY: Float,
|
||||
)
|
||||
|
||||
/**
|
||||
@ -50,7 +50,7 @@ interface SwipeDetector {
|
||||
/**
|
||||
* swipe along the Y- Axes
|
||||
*/
|
||||
VERTICAL
|
||||
VERTICAL,
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ interface SwipeDetector {
|
||||
* @param swipeMagnitudeThreshold minimum magnitude before a swipe is detected as such
|
||||
*/
|
||||
class SwipeDetectorImpl(
|
||||
private val swipeMagnitudeThreshold: Double
|
||||
private val swipeMagnitudeThreshold: Double,
|
||||
) : SwipeDetector {
|
||||
override var currentSwipe = SwipeDetector.SwipeDirection.NONE
|
||||
|
||||
@ -68,7 +68,7 @@ class SwipeDetectorImpl(
|
||||
from: MotionEvent,
|
||||
to: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
distanceY: Float,
|
||||
) {
|
||||
if (currentSwipe == SwipeDetector.SwipeDirection.NONE) {
|
||||
// no swipe direction was detected yet, try to detect one
|
||||
|
@ -48,7 +48,7 @@ class VolumeAndBrightnessScrollerImpl(
|
||||
private val screenController: ScreenBrightnessController?,
|
||||
private val overlayController: SwipeControlsOverlay,
|
||||
volumeDistance: Int = 10,
|
||||
brightnessDistance: Int = 1
|
||||
brightnessDistance: Int = 1,
|
||||
) : VolumeAndBrightnessScroller {
|
||||
|
||||
// region volume
|
||||
@ -56,8 +56,8 @@ class VolumeAndBrightnessScrollerImpl(
|
||||
ScrollDistanceHelper(
|
||||
volumeDistance.applyDimension(
|
||||
context,
|
||||
TypedValue.COMPLEX_UNIT_DIP
|
||||
)
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
),
|
||||
) { _, _, direction ->
|
||||
volumeController?.run {
|
||||
volume += direction
|
||||
@ -73,8 +73,8 @@ class VolumeAndBrightnessScrollerImpl(
|
||||
ScrollDistanceHelper(
|
||||
brightnessDistance.applyDimension(
|
||||
context,
|
||||
TypedValue.COMPLEX_UNIT_DIP
|
||||
)
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
),
|
||||
) { _, _, direction ->
|
||||
screenController?.run {
|
||||
if (screenBrightness > 0 || direction > 0) {
|
||||
|
@ -7,7 +7,7 @@ import android.view.MotionEvent
|
||||
*/
|
||||
data class Point(
|
||||
val x: Int,
|
||||
val y: Int
|
||||
val y: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@ data class Rectangle(
|
||||
val x: Int,
|
||||
val y: Int,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
val height: Int,
|
||||
) {
|
||||
val left = x
|
||||
val right = x + width
|
||||
@ -15,7 +15,6 @@ data class Rectangle(
|
||||
val bottom = y + height
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* is the point within this rectangle?
|
||||
*/
|
||||
|
@ -11,7 +11,7 @@ import kotlin.math.sign
|
||||
*/
|
||||
class ScrollDistanceHelper(
|
||||
private val unitDistance: Int,
|
||||
private val callback: (oldDistance: Double, newDistance: Double, direction: Int) -> Unit
|
||||
private val callback: (oldDistance: Double, newDistance: Double, direction: Int) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -35,7 +35,7 @@ class ScrollDistanceHelper(
|
||||
callback.invoke(
|
||||
oldDistance,
|
||||
scrolledDistance,
|
||||
sign(scrolledDistance).toInt()
|
||||
sign(scrolledDistance).toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -53,4 +53,4 @@ class ScrollDistanceHelper(
|
||||
private fun subtractUnitDistance() {
|
||||
scrolledDistance -= (unitDistance * sign(scrolledDistance))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,4 +23,4 @@ interface SwipeControlsOverlay {
|
||||
* called when a new swipe- session has started
|
||||
*/
|
||||
fun onEnterSwipeSession()
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ fun Int.applyDimension(context: Context, unit: Int): Int {
|
||||
return TypedValue.applyDimension(
|
||||
unit,
|
||||
this.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
context.resources.displayMetrics,
|
||||
).roundToInt()
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,10 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import app.revanced.integrations.shared.Utils
|
||||
import app.revanced.integrations.youtube.swipecontrols.SwipeControlsConfigurationProvider
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.SwipeControlsOverlay
|
||||
import app.revanced.integrations.youtube.swipecontrols.misc.applyDimension
|
||||
import app.revanced.integrations.shared.Utils
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
@ -24,7 +24,7 @@ import kotlin.math.round
|
||||
*/
|
||||
class SwipeControlsOverlayLayout(
|
||||
context: Context,
|
||||
private val config: SwipeControlsConfigurationProvider
|
||||
private val config: SwipeControlsConfigurationProvider,
|
||||
) : RelativeLayout(context), SwipeControlsOverlay {
|
||||
/**
|
||||
* DO NOT use this, for tools only
|
||||
@ -40,14 +40,14 @@ class SwipeControlsOverlayLayout(
|
||||
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
|
||||
return resources.getDrawable(
|
||||
Utils.getResourceIdentifier(context, name, "drawable"),
|
||||
context.theme
|
||||
context.theme,
|
||||
).apply {
|
||||
setTint(config.overlayForegroundColor)
|
||||
setBounds(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
height,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -59,14 +59,14 @@ class SwipeControlsOverlayLayout(
|
||||
feedbackTextView = TextView(context).apply {
|
||||
layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
addRule(CENTER_IN_PARENT, TRUE)
|
||||
setPadding(
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding
|
||||
feedbackTextViewPadding,
|
||||
)
|
||||
}
|
||||
background = GradientDrawable().apply {
|
||||
@ -108,7 +108,7 @@ class SwipeControlsOverlayLayout(
|
||||
icon,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
)
|
||||
visibility = VISIBLE
|
||||
}
|
||||
@ -117,7 +117,7 @@ class SwipeControlsOverlayLayout(
|
||||
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
||||
showFeedbackView(
|
||||
"$newVolume",
|
||||
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon
|
||||
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,8 +134,8 @@ class SwipeControlsOverlayLayout(
|
||||
@Suppress("DEPRECATION")
|
||||
performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,14 @@
|
||||
package app.revanced.integrations.youtube.videoplayer;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.youtube.patches.DownloadsPatch;
|
||||
import app.revanced.integrations.youtube.patches.VideoInformation;
|
||||
import app.revanced.integrations.youtube.settings.Settings;
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
import app.revanced.integrations.shared.StringRef;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ExternalDownloadButton extends BottomControlButton {
|
||||
@ -47,39 +44,10 @@ public class ExternalDownloadButton extends BottomControlButton {
|
||||
}
|
||||
|
||||
private static void onDownloadClick(View view) {
|
||||
Logger.printDebug(() -> "External download button clicked");
|
||||
|
||||
final var context = view.getContext();
|
||||
// Trim string to avoid any accidental whitespace.
|
||||
var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim();
|
||||
|
||||
boolean packageEnabled = false;
|
||||
try {
|
||||
packageEnabled = context.getPackageManager().getApplicationInfo(downloaderPackageName, 0).enabled;
|
||||
} catch (PackageManager.NameNotFoundException error) {
|
||||
Logger.printDebug(() -> "External downloader could not be found: " + error);
|
||||
}
|
||||
|
||||
// If the package is not installed, show the toast
|
||||
if (!packageEnabled) {
|
||||
Utils.showToastLong(StringRef.str("revanced_external_downloader_not_installed_warning", downloaderPackageName));
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch PowerTube intent
|
||||
try {
|
||||
String content = String.format("https://youtu.be/%s", VideoInformation.getVideoId());
|
||||
|
||||
Intent intent = new Intent("android.intent.action.SEND");
|
||||
intent.setType("text/plain");
|
||||
intent.setPackage(downloaderPackageName);
|
||||
intent.putExtra("android.intent.extra.TEXT", content);
|
||||
context.startActivity(intent);
|
||||
|
||||
Logger.printDebug(() -> "Launched the intent with the content: " + content);
|
||||
} catch (Exception error) {
|
||||
Logger.printException(() -> "Failed to launch the intent: " + error, error);
|
||||
}
|
||||
DownloadsPatch.launchExternalDownloader(
|
||||
VideoInformation.getVideoId(),
|
||||
view.getContext(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
android.useAndroidX = true
|
||||
version = 1.4.0
|
||||
version = 1.5.0-dev.10
|
||||
|
@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.2.2"
|
||||
agp = "8.2.2" # 8.3.0 causes java verifier error: https://github.com/ReVanced/revanced-patches/issues/2818
|
||||
annotation = "1.7.1"
|
||||
kotlin = "1.9.22"
|
||||
appcompat = "1.7.0-alpha03"
|
||||
|
@ -16,7 +16,7 @@ android {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user