chore: Merge branch `dev` to `main` (#577)

This commit is contained in:
oSumAtrIX 2024-03-27 19:35:42 +01:00 committed by GitHub
commit 7760a39602
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 1326 additions and 479 deletions

3
.editorconfig Normal file
View File

@ -0,0 +1,3 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

View File

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

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,4 +6,4 @@ abstract class BaseJsonHook : JsonHook {
abstract fun apply(json: JSONObject)
override fun transform(json: JSONObject) = json.apply { apply(json) }
}
}

View File

@ -12,4 +12,4 @@ interface JsonHook : Hook<JSONObject> {
fun transform(json: JSONObject): JSONObject
override fun hook(type: JSONObject) = transform(type)
}
}

View File

@ -27,4 +27,4 @@ object JsonHookPatch {
return StreamUtils.fromString(jsonObject.toString())
}
}
}

View File

@ -6,4 +6,4 @@ interface Hook<T> {
* @param type The type to hook
*/
fun hook(type: T): T
}
}

View File

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

View File

@ -11,4 +11,4 @@ object DummyHook : BaseJsonHook() {
override fun apply(json: JSONObject) {
// Do nothing.
}
}
}

View File

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

View File

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

View File

@ -10,4 +10,4 @@ object JsonUtils {
@JvmStatic
@Throws(IOException::class, JSONException::class)
fun parseJson(jsonInputStream: InputStream) = JSONObject(StreamUtils.toString(jsonInputStream))
}
}

View File

@ -21,4 +21,4 @@ object StreamUtils {
fun fromString(string: String): InputStream {
return ByteArrayInputStream(string.toByteArray())
}
}
}

View File

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

View File

@ -27,4 +27,3 @@ class Event<T> {
observer.invoke(value)
}
}

View File

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

View File

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

View File

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

View File

@ -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 {
/**

View File

@ -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.

View File

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

View File

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

View File

@ -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.

View File

@ -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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

@ -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.

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
@Deprecated
public final class StoryboardRenderer {
@Nullable
private final String spec;

View File

@ -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(

View File

@ -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 {
/**

View File

@ -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.

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
)

View File

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

View File

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

View File

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

View File

@ -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 {
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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) {

View File

@ -7,7 +7,7 @@ import android.view.MotionEvent
*/
data class Point(
val x: Int,
val y: Int
val y: Int,
)
/**

View File

@ -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?
*/

View File

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

View File

@ -23,4 +23,4 @@ interface SwipeControlsOverlay {
* called when a new swipe- session has started
*/
fun onEnterSwipeSession()
}
}

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -16,7 +16,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}