├── CHANGELOG.md ├── app ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── app │ │ └── revanced │ │ └── integrations │ │ ├── music │ │ ├── patches │ │ │ ├── misc │ │ │ │ ├── OpusCodecPatch.java │ │ │ │ ├── CairoSplashAnimationPatch.java │ │ │ │ └── ShareSheetPatch.java │ │ │ ├── utils │ │ │ │ ├── PatchStatus.java │ │ │ │ ├── VideoTypeHookPatch.java │ │ │ │ ├── PlayerTypeHookPatch.java │ │ │ │ ├── DrawableColorPatch.java │ │ │ │ └── InitializationPatch.java │ │ │ ├── ads │ │ │ │ ├── MusicAdsPatch.java │ │ │ │ ├── PremiumPromotionPatch.java │ │ │ │ └── PremiumRenewalPatch.java │ │ │ ├── components │ │ │ │ ├── PlayerFlyoutMenuFilter.java │ │ │ │ ├── PlayerComponentsFilter.java │ │ │ │ ├── AdsFilter.java │ │ │ │ ├── ShareSheetMenuFilter.java │ │ │ │ └── LayoutComponentsFilter.java │ │ │ ├── video │ │ │ │ └── PlaybackSpeedPatch.java │ │ │ ├── general │ │ │ │ └── SettingsMenuPatch.java │ │ │ ├── navigation │ │ │ │ └── NavigationPatch.java │ │ │ └── account │ │ │ │ └── AccountPatch.java │ │ ├── utils │ │ │ ├── RestartUtils.java │ │ │ └── ExtendedUtils.java │ │ ├── shared │ │ │ ├── PlayerType.kt │ │ │ └── VideoType.kt │ │ ├── sponsorblock │ │ │ ├── SponsorBlockSettings.java │ │ │ └── objects │ │ │ │ └── CategoryBehaviour.java │ │ └── settings │ │ │ └── preference │ │ │ └── ResettableEditTextPreference.java │ │ ├── youtube │ │ ├── patches │ │ │ ├── misc │ │ │ │ ├── OpusCodecPatch.java │ │ │ │ ├── BackgroundPlaybackPatch.java │ │ │ │ ├── ExternalBrowserPatch.java │ │ │ │ ├── QUICProtocolPatch.java │ │ │ │ ├── OpenLinksDirectlyPatch.java │ │ │ │ ├── WatchHistoryPatch.java │ │ │ │ └── client │ │ │ │ │ └── DeviceHardwareSupport.java │ │ │ ├── video │ │ │ │ ├── HDRVideoPatch.java │ │ │ │ ├── VP9CodecPatch.java │ │ │ │ ├── SpoofDeviceDimensionsPatch.java │ │ │ │ ├── AV1CodecPatch.java │ │ │ │ └── ReloadVideoPatch.java │ │ │ ├── utils │ │ │ │ ├── LockModeStateHookPatch.java │ │ │ │ ├── BottomSheetHookPatch.java │ │ │ │ ├── PlayerControlsVisibilityHookPatch.java │ │ │ │ ├── ToolBarPatch.java │ │ │ │ ├── LottieAnimationViewPatch.java │ │ │ │ ├── AlwaysRepeatPatch.java │ │ │ │ ├── CastButtonPatch.java │ │ │ │ ├── ProgressBarDrawable.java │ │ │ │ ├── PatchStatus.java │ │ │ │ ├── InitializationPatch.java │ │ │ │ ├── PlayerTypeHookPatch.java │ │ │ │ ├── DrawableColorPatch.java │ │ │ │ └── DoubleBackToClosePatch.java │ │ │ ├── components │ │ │ │ ├── ShareSheetMenuFilter.java │ │ │ │ ├── VideoQualityMenuFilter.java │ │ │ │ ├── LayoutComponentsFilter.java │ │ │ │ └── PlaybackSpeedMenuFilter.java │ │ │ ├── ads │ │ │ │ └── AdsPatch.java │ │ │ ├── swipe │ │ │ │ └── SwipeControlsPatch.java │ │ │ ├── spans │ │ │ │ ├── SanitizeVideoSubtitleFilter.java │ │ │ │ └── SearchLinksFilter.java │ │ │ ├── feed │ │ │ │ └── RelatedVideoPatch.java │ │ │ ├── overlaybutton │ │ │ │ ├── ExternalDownload.java │ │ │ │ ├── CopyVideoUrl.java │ │ │ │ ├── PlayAll.java │ │ │ │ ├── CopyVideoUrlTimestamp.java │ │ │ │ ├── Whitelists.java │ │ │ │ ├── AlwaysRepeat.java │ │ │ │ └── SpeedDialog.java │ │ │ └── general │ │ │ │ ├── YouTubeMusicActionsPatch.java │ │ │ │ └── LayoutSwitchPatch.java │ │ ├── swipecontrols │ │ │ ├── misc │ │ │ │ ├── Point.kt │ │ │ │ ├── Rectangle.kt │ │ │ │ ├── SwipeControlsOverlay.kt │ │ │ │ ├── SwipeControlsUtils.kt │ │ │ │ └── ScrollDistanceHelper.kt │ │ │ └── controller │ │ │ │ ├── gesture │ │ │ │ └── core │ │ │ │ │ └── GestureController.kt │ │ │ │ ├── VolumeKeysController.kt │ │ │ │ └── ScreenBrightnessController.kt │ │ ├── whitelist │ │ │ └── VideoChannel.java │ │ ├── shared │ │ │ ├── PlaylistIdPrefix.java │ │ │ ├── VideoState.kt │ │ │ ├── BottomSheetState.kt │ │ │ ├── ShortsPlayerState.kt │ │ │ ├── RootView.java │ │ │ ├── PlayerControlsVisibility.kt │ │ │ └── LockModeState.kt │ │ ├── settings │ │ │ └── preference │ │ │ │ ├── AlternativeThumbnailsAboutDeArrowPreference.java │ │ │ │ ├── AboutYouTubeDataAPIPreference.java │ │ │ │ └── OpenDefaultAppSettingsPreference.java │ │ └── sponsorblock │ │ │ └── objects │ │ │ └── UserStats.java │ │ ├── reddit │ │ ├── patches │ │ │ ├── SanitizeUrlQueryPatch.java │ │ │ ├── RecommendedCommunitiesPatch.java │ │ │ ├── ScreenshotPopupPatch.java │ │ │ ├── ToolBarButtonPatch.java │ │ │ ├── RecentlyVisitedShelfPatch.java │ │ │ ├── TrendingTodayShelfPatch.java │ │ │ ├── OpenLinksDirectlyPatch.java │ │ │ ├── OpenLinksExternallyPatch.java │ │ │ └── GeneralAdsPatch.java │ │ └── settings │ │ │ ├── preference │ │ │ ├── TogglePreference.java │ │ │ ├── categories │ │ │ │ ├── ConditionalPreferenceCategory.java │ │ │ │ ├── AdsPreferenceCategory.java │ │ │ │ └── MiscellaneousPreferenceCategory.java │ │ │ └── ReVancedPreferenceFragment.java │ │ │ ├── ActivityHook.java │ │ │ └── Settings.java │ │ └── shared │ │ ├── patches │ │ ├── components │ │ │ ├── StringFilterGroupList.java │ │ │ ├── ByteArrayFilterGroupList.java │ │ │ ├── StringFilterGroup.java │ │ │ └── FilterGroupList.java │ │ ├── BaseSettingsMenuPatch.java │ │ ├── spans │ │ │ ├── SpanType.java │ │ │ ├── StringFilterGroup.java │ │ │ ├── FilterGroupList.java │ │ │ └── FilterGroup.java │ │ ├── AutoCaptionsPatch.java │ │ ├── BypassImageRegionRestrictionsPatch.java │ │ ├── SanitizeUrlQueryPatch.java │ │ └── FullscreenAdsPatch.java │ │ ├── returnyoutubedislike │ │ ├── ReturnYouTubeDislike.java │ │ └── requests │ │ │ └── ReturnYouTubeDislikeRoutes.java │ │ ├── utils │ │ ├── Event.kt │ │ ├── StringTrieSearch.java │ │ ├── ByteTrieSearch.java │ │ ├── IntentUtils.java │ │ └── BaseThemeUtils.java │ │ ├── returnyoutubeusername │ │ └── requests │ │ │ └── ChannelRoutes.java │ │ ├── settings │ │ └── preference │ │ │ ├── HtmlPreference.java │ │ │ ├── WideListPreference.java │ │ │ └── WebViewDialog.java │ │ ├── sponsorblock │ │ └── requests │ │ │ └── SBRoutes.java │ │ └── requests │ │ └── Route.java ├── proguard-rules.pro └── build.gradle.kts ├── stub ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ ├── org │ │ └── chromium │ │ │ └── net │ │ │ ├── UrlRequest.java │ │ │ ├── UrlResponseInfo.java │ │ │ └── impl │ │ │ └── CronetUrlRequest.java │ │ ├── com │ │ ├── google │ │ │ └── android │ │ │ │ ├── apps │ │ │ │ └── youtube │ │ │ │ │ └── app │ │ │ │ │ ├── settings │ │ │ │ │ └── SettingsActivity.java │ │ │ │ │ └── application │ │ │ │ │ └── Shell_SettingsActivity.java │ │ │ │ ├── material │ │ │ │ └── textfield │ │ │ │ │ └── TextInputLayout.java │ │ │ │ └── libraries │ │ │ │ └── youtube │ │ │ │ └── rendering │ │ │ │ └── ui │ │ │ │ └── pivotbar │ │ │ │ └── PivotBar.java │ │ ├── reddit │ │ │ └── domain │ │ │ │ └── model │ │ │ │ ├── Link.java │ │ │ │ └── ILink.java │ │ └── airbnb │ │ │ └── lottie │ │ │ └── LottieAnimationView.java │ │ ├── android │ │ └── support │ │ │ └── v7 │ │ │ └── widget │ │ │ ├── Toolbar.java │ │ │ └── RecyclerView.java │ │ └── androidx │ │ └── coordinatorlayout │ │ └── widget │ │ └── CoordinatorLayout.java ├── build.gradle.kts └── proguard-rules.pro ├── gradle.properties ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitattributes ├── package.json ├── .gitignore ├── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-issue.yml │ └── bug-issue.yml └── workflows │ └── discord_ping_on_release.yml ├── settings.gradle.kts └── .releaserc /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /stub/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /stub/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel = true 2 | org.gradle.caching = true 3 | android.useAndroidX = true 4 | version = 1.18.1 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inotia00/revanced-integrations/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /stub/src/main/java/org/chromium/net/UrlRequest.java: -------------------------------------------------------------------------------- 1 | package org.chromium.net; 2 | 3 | public abstract class UrlRequest { 4 | } 5 | -------------------------------------------------------------------------------- /stub/src/main/java/com/google/android/apps/youtube/app/settings/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.youtube.app.settings; 2 | 3 | public class SettingsActivity { 4 | } -------------------------------------------------------------------------------- /stub/src/main/java/com/google/android/apps/youtube/app/application/Shell_SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.youtube.app.application; 2 | 3 | public class Shell_SettingsActivity { 4 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@saithodev/semantic-release-backmerge": "^4.0.1", 4 | "@semantic-release/changelog": "^6.0.3", 5 | "@semantic-release/git": "^10.0.1", 6 | "gradle-semantic-release-plugin": "^1.9.2", 7 | "semantic-release": "^24.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /stub/src/main/java/org/chromium/net/UrlResponseInfo.java: -------------------------------------------------------------------------------- 1 | package org.chromium.net; 2 | 3 | //dummy class 4 | public abstract class UrlResponseInfo { 5 | 6 | public abstract String getUrl(); 7 | 8 | public abstract int getHttpStatusCode(); 9 | 10 | // Add additional existing methods, if needed. 11 | 12 | } 13 | -------------------------------------------------------------------------------- /stub/src/main/java/org/chromium/net/impl/CronetUrlRequest.java: -------------------------------------------------------------------------------- 1 | package org.chromium.net.impl; 2 | 3 | import org.chromium.net.UrlRequest; 4 | 5 | public abstract class CronetUrlRequest extends UrlRequest { 6 | 7 | /** 8 | * Method is added by patch. 9 | */ 10 | public abstract String getHookedUrl(); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /build 11 | /captures 12 | .externalNativeBuild 13 | .cxx 14 | /.idea 15 | /.vscode 16 | /*.log 17 | node_modules 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /stub/src/main/java/com/google/android/material/textfield/TextInputLayout.java: -------------------------------------------------------------------------------- 1 | package com.google.android.material.textfield; 2 | 3 | import android.content.Context; 4 | import android.widget.LinearLayout; 5 | 6 | public class TextInputLayout extends LinearLayout { 7 | 8 | public TextInputLayout(Context context) { 9 | super(context); 10 | } 11 | } -------------------------------------------------------------------------------- /stub/src/main/java/com/reddit/domain/model/Link.java: -------------------------------------------------------------------------------- 1 | package com.reddit.domain.model; 2 | 3 | public class Link { 4 | public boolean getPromoted() { 5 | return false; 6 | } 7 | 8 | public boolean isBlankAd() { 9 | return false; 10 | } 11 | 12 | public Boolean isSurveyAd() { 13 | return Boolean.FALSE; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /stub/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java: -------------------------------------------------------------------------------- 1 | package com.google.android.libraries.youtube.rendering.ui.pivotbar; 2 | 3 | import android.content.Context; 4 | import android.widget.HorizontalScrollView; 5 | 6 | public class PivotBar extends HorizontalScrollView { 7 | public PivotBar(Context context) { 8 | super(context); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/misc/OpusCodecPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.misc; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class OpusCodecPatch { 7 | 8 | public static boolean enableOpusCodec() { 9 | return Settings.ENABLE_OPUS_CODEC.get(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/OpusCodecPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class OpusCodecPatch { 7 | 8 | public static boolean enableOpusCodec() { 9 | return Settings.ENABLE_OPUS_CODEC.get(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/video/HDRVideoPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.video; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class HDRVideoPatch { 7 | 8 | public static boolean disableHDRVideo() { 9 | return !Settings.DISABLE_HDR_VIDEO.get(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/video/VP9CodecPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.video; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class VP9CodecPatch { 7 | 8 | public static boolean disableVP9Codec() { 9 | return !Settings.DISABLE_VP9_CODEC.get(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔩 ReVanced Integrations 2 | 3 | ReVanced Extended Integrations containing classes to be merged by ReVanced Patcher. 4 | 5 | ## ❓ How to use debugging: 6 | 7 | - Usage on Windows: ```adb logcat | findstr "Extended" > log.txt``` 8 | - Usage on Linux: ```adb logcat | grep --line-buffered "Extended" > log.txt``` 9 | 10 | This will write the log to a file called log.txt which you can view then. 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/utils/PatchStatus.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.utils; 2 | 3 | @SuppressWarnings("unused") 4 | public class PatchStatus { 5 | public static boolean SpoofAppVersionDefaultBoolean() { 6 | return false; 7 | } 8 | 9 | public static String SpoofAppVersionDefaultString() { 10 | return "6.11.52"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/SanitizeUrlQueryPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import app.revanced.integrations.reddit.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class SanitizeUrlQueryPatch { 7 | 8 | public static boolean stripQueryParameters() { 9 | return Settings.SANITIZE_URL_QUERY.get(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java: -------------------------------------------------------------------------------- 1 | package com.airbnb.lottie; 2 | 3 | import android.content.Context; 4 | import android.widget.ImageView; 5 | 6 | public class LottieAnimationView extends ImageView { 7 | 8 | public LottieAnimationView(Context context) { 9 | super(context); 10 | } 11 | 12 | @SuppressWarnings("unused") 13 | public void setAnimation(final int rawRes) { 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/components/StringFilterGroupList.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.components; 2 | 3 | import app.revanced.integrations.shared.utils.StringTrieSearch; 4 | 5 | public final class StringFilterGroupList extends FilterGroupList { 6 | protected StringTrieSearch createSearchGraph() { 7 | return new StringTrieSearch(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📃 Documentation 4 | url: https://github.com/revanced/revanced-documentation/ 5 | about: Don't know how or where to start? Check out our documentation! 6 | - name: 🗨 Discussions 7 | url: https://github.com/revanced/revanced-suggestions/discussions 8 | about: Got something you think should change or be added? Search for or start a new discussion! 9 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/RecommendedCommunitiesPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import app.revanced.integrations.reddit.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class RecommendedCommunitiesPatch { 7 | 8 | public static boolean hideRecommendedCommunitiesShelf() { 9 | return Settings.HIDE_RECOMMENDED_COMMUNITIES_SHELF.get(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/ScreenshotPopupPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import app.revanced.integrations.reddit.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class ScreenshotPopupPatch { 7 | 8 | public static Boolean disableScreenshotPopup(Boolean original) { 9 | return Settings.DISABLE_SCREENSHOT_POPUP.get() ? Boolean.FALSE : original; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/returnyoutubedislike/ReturnYouTubeDislike.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.returnyoutubedislike; 2 | 3 | public class ReturnYouTubeDislike { 4 | 5 | public enum Vote { 6 | LIKE(1), 7 | DISLIKE(-1), 8 | LIKE_REMOVE(0); 9 | 10 | public final int value; 11 | 12 | Vote(int value) { 13 | this.value = value; 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/misc/Point.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.misc 2 | 3 | import android.view.MotionEvent 4 | 5 | /** 6 | * a simple 2D point class 7 | */ 8 | data class Point( 9 | val x: Int, 10 | val y: Int, 11 | ) 12 | 13 | /** 14 | * convert the motion event coordinates to a point 15 | */ 16 | fun MotionEvent.toPoint(): Point = 17 | Point(x.toInt(), y.toInt()) 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/misc/CairoSplashAnimationPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.misc; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class CairoSplashAnimationPatch { 7 | 8 | public static boolean disableCairoSplashAnimation(boolean original) { 9 | return !Settings.DISABLE_CAIRO_SPLASH_ANIMATION.get() && original; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/BackgroundPlaybackPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc; 2 | 3 | import app.revanced.integrations.youtube.shared.ShortsPlayerState; 4 | 5 | @SuppressWarnings("unused") 6 | public class BackgroundPlaybackPatch { 7 | 8 | public static boolean allowBackgroundPlayback(boolean original) { 9 | return original || ShortsPlayerState.getCurrent().isClosed(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/ExternalBrowserPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class ExternalBrowserPatch { 7 | 8 | public static String enableExternalBrowser(final String original) { 9 | if (!Settings.ENABLE_EXTERNAL_BROWSER.get()) 10 | return original; 11 | 12 | return ""; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/ToolBarButtonPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.reddit.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class ToolBarButtonPatch { 9 | 10 | public static void hideToolBarButton(View view) { 11 | if (!Settings.HIDE_TOOLBAR_BUTTON.get()) 12 | return; 13 | 14 | view.setVisibility(View.GONE); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/ads/MusicAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.ads; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class MusicAdsPatch { 7 | 8 | public static boolean hideMusicAds() { 9 | return !Settings.HIDE_MUSIC_ADS.get(); 10 | } 11 | 12 | public static boolean hideMusicAds(boolean original) { 13 | return !Settings.HIDE_MUSIC_ADS.get() && original; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /stub/src/main/java/com/reddit/domain/model/ILink.java: -------------------------------------------------------------------------------- 1 | package com.reddit.domain.model; 2 | 3 | public class ILink { 4 | public long getCreatedUtc() { 5 | return -1L; 6 | } 7 | 8 | public String getId() { 9 | return ""; 10 | } 11 | 12 | public boolean getPromoted() { 13 | throw new UnsupportedOperationException("Stub"); 14 | } 15 | 16 | public String getUniqueId() { 17 | return ""; 18 | } 19 | 20 | public boolean isBlankAd() { 21 | return false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/BaseSettingsMenuPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches; 2 | 3 | import android.util.Log; 4 | 5 | import androidx.preference.PreferenceScreen; 6 | 7 | @SuppressWarnings("unused") 8 | public class BaseSettingsMenuPatch { 9 | 10 | /** 11 | * Rest of the implementation added by patch. 12 | */ 13 | public static void removePreference(PreferenceScreen mPreferenceScreen, String key) { 14 | Log.d("Extended: SettingsMenuPatch", "key: " + key); 15 | } 16 | } -------------------------------------------------------------------------------- /stub/src/main/java/android/support/v7/widget/Toolbar.java: -------------------------------------------------------------------------------- 1 | package android.support.v7.widget; 2 | 3 | import android.content.Context; 4 | import android.view.ViewGroup; 5 | 6 | /** 7 | * "CompileOnly" class 8 | *

9 | * This class will not be included and "replaced" by the real package's class. 10 | */ 11 | public class Toolbar extends ViewGroup { 12 | public Toolbar(Context context) { 13 | super(context); 14 | } 15 | 16 | @Override 17 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "revanced-integrations" 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | @Suppress("UnstableApiUsage") 11 | dependencyResolutionManagement { 12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | buildCache { 20 | local { 21 | isEnabled = "CI" !in System.getenv() 22 | } 23 | } 24 | 25 | include(":app") 26 | include(":stub") 27 | -------------------------------------------------------------------------------- /stub/src/main/java/android/support/v7/widget/RecyclerView.java: -------------------------------------------------------------------------------- 1 | package android.support.v7.widget; 2 | 3 | import android.content.Context; 4 | import android.view.ViewGroup; 5 | 6 | /** 7 | * "CompileOnly" class 8 | *

9 | * This class will not be included and "replaced" by the real package's class. 10 | */ 11 | public class RecyclerView extends ViewGroup { 12 | public RecyclerView(Context context) { 13 | super(context); 14 | } 15 | 16 | @Override 17 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/RecentlyVisitedShelfPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | import app.revanced.integrations.reddit.settings.Settings; 7 | 8 | @SuppressWarnings("unused") 9 | @Deprecated(forRemoval = true) 10 | public final class RecentlyVisitedShelfPatch { 11 | 12 | public static List hideRecentlyVisitedShelf(List list) { 13 | return Settings.HIDE_RECENTLY_VISITED_SHELF.get() ? Collections.emptyList() : list; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/misc/Rectangle.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.misc 2 | 3 | /** 4 | * a simple rectangle class 5 | */ 6 | data class Rectangle( 7 | val x: Int, 8 | val y: Int, 9 | val width: Int, 10 | val height: Int, 11 | ) { 12 | val left = x 13 | val right = x + width 14 | val top = y 15 | val bottom = y + height 16 | } 17 | 18 | /** 19 | * is the point within this rectangle? 20 | */ 21 | operator fun Rectangle.contains(p: Point): Boolean = 22 | p.x in left..right && p.y in top..bottom 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/utils/VideoTypeHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.utils; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.music.shared.VideoType; 6 | 7 | @SuppressWarnings("unused") 8 | public class VideoTypeHookPatch { 9 | /** 10 | * Injection point. 11 | */ 12 | public static void setVideoType(@Nullable Enum musicVideoType) { 13 | if (musicVideoType == null) 14 | return; 15 | 16 | VideoType.setFromString(musicVideoType.name()); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/LockModeStateHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.youtube.shared.LockModeState; 6 | 7 | @SuppressWarnings("unused") 8 | public class LockModeStateHookPatch { 9 | /** 10 | * Injection point. 11 | */ 12 | public static void setLockModeState(@Nullable Enum lockModeState) { 13 | if (lockModeState == null) return; 14 | 15 | LockModeState.setFromString(lockModeState.name()); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/utils/PlayerTypeHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.utils; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.music.shared.PlayerType; 6 | 7 | @SuppressWarnings("unused") 8 | public class PlayerTypeHookPatch { 9 | /** 10 | * Injection point. 11 | */ 12 | public static void setPlayerType(@Nullable Enum musicPlayerType) { 13 | if (musicPlayerType == null) 14 | return; 15 | 16 | PlayerType.setFromString(musicPlayerType.name()); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/spans/SpanType.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.spans; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | public enum SpanType { 6 | CLICKABLE("ClickableSpan"), 7 | FOREGROUND_COLOR("ForegroundColorSpan"), 8 | ABSOLUTE_SIZE("AbsoluteSizeSpan"), 9 | TYPEFACE("TypefaceSpan"), 10 | IMAGE("ImageSpan"), 11 | CUSTOM_CHARACTER_STYLE("CustomCharacterStyle"), 12 | UNKNOWN("Unknown"); 13 | 14 | @NonNull 15 | public final String type; 16 | 17 | SpanType(@NonNull String type) { 18 | this.type = type; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/AutoCaptionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches; 2 | 3 | import app.revanced.integrations.shared.settings.BaseSettings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class AutoCaptionsPatch { 7 | 8 | private static boolean captionsButtonStatus; 9 | 10 | public static boolean disableAutoCaptions() { 11 | return BaseSettings.DISABLE_AUTO_CAPTIONS.get() && 12 | !captionsButtonStatus; 13 | } 14 | 15 | public static void setCaptionsButtonStatus(boolean status) { 16 | captionsButtonStatus = status; 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/gesture/core/GestureController.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.controller.gesture.core 2 | 3 | import android.view.MotionEvent 4 | 5 | /** 6 | * describes a class that accepts motion events and detects gestures 7 | */ 8 | interface GestureController { 9 | /** 10 | * accept a touch event and try to detect the desired gestures using it 11 | * 12 | * @param motionEvent the motion event that was submitted 13 | * @return was a gesture detected? 14 | */ 15 | fun submitTouchEvent(motionEvent: MotionEvent): Boolean 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/BottomSheetHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import app.revanced.integrations.youtube.shared.BottomSheetState; 4 | 5 | @SuppressWarnings("unused") 6 | public class BottomSheetHookPatch { 7 | /** 8 | * Injection point. 9 | */ 10 | public static void onAttachedToWindow() { 11 | BottomSheetState.set(BottomSheetState.OPEN); 12 | } 13 | 14 | /** 15 | * Injection point. 16 | */ 17 | public static void onDetachedFromWindow() { 18 | BottomSheetState.set(BottomSheetState.CLOSED); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/video/SpoofDeviceDimensionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.video; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class SpoofDeviceDimensionsPatch { 7 | private static final boolean SPOOF = Settings.SPOOF_DEVICE_DIMENSIONS.get(); 8 | 9 | public static int getMinHeightOrWidth(int minHeightOrWidth) { 10 | return SPOOF ? 64 : minHeightOrWidth; 11 | } 12 | 13 | public static int getMaxHeightOrWidth(int maxHeightOrWidth) { 14 | return SPOOF ? 4096 : maxHeightOrWidth; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/whitelist/VideoChannel.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.whitelist; 2 | 3 | import java.io.Serializable; 4 | 5 | public final class VideoChannel implements Serializable { 6 | private final String channelName; 7 | private final String channelId; 8 | 9 | public VideoChannel(String channelName, String channelId) { 10 | this.channelName = channelName; 11 | this.channelId = channelId; 12 | } 13 | 14 | public String getChannelName() { 15 | return channelName; 16 | } 17 | 18 | public String getChannelId() { 19 | return channelId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -dontoptimize 3 | -keepattributes * 4 | -keep class app.revanced.** { 5 | *; 6 | } 7 | -keep class com.google.** { 8 | *; 9 | } 10 | -keep class org.schabi.newpipe.extractor.timeago.patterns.** { 11 | *; 12 | } 13 | -keep class org.mozilla.javascript.** { 14 | *; 15 | } 16 | -keep class org.mozilla.classfile.ClassFileWriter 17 | -dontwarn java.awt.** 18 | -dontwarn javax.swing.** 19 | -dontwarn org.mozilla.javascript.tools.** 20 | -dontwarn java.beans.BeanDescriptor 21 | -dontwarn java.beans.BeanInfo 22 | -dontwarn java.beans.IntrospectionException 23 | -dontwarn java.beans.Introspector 24 | -dontwarn java.beans.PropertyDescriptor 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/components/ByteArrayFilterGroupList.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.components; 2 | 3 | import app.revanced.integrations.shared.utils.ByteTrieSearch; 4 | 5 | /** 6 | * If searching for a single byte pattern, then it is slightly better to use 7 | * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster 8 | * than a prefix tree to search for only 1 pattern. 9 | */ 10 | public final class ByteArrayFilterGroupList extends FilterGroupList { 11 | protected ByteTrieSearch createSearchGraph() { 12 | return new ByteTrieSearch(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.2.2" 3 | annotation = "1.9.1" 4 | lang3 = "3.19.0" 5 | kotlin = "2.0.21" 6 | preference = "1.2.1" 7 | 8 | [libraries] 9 | annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } 10 | lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" } 11 | preference = { module = "androidx.preference:preference", version.ref = "preference" } 12 | 13 | [plugins] 14 | android-application = { id = "com.android.application", version.ref = "agp" } 15 | android-library = { id = "com.android.library", version.ref = "agp" } 16 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/QUICProtocolPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc; 2 | 3 | import app.revanced.integrations.shared.utils.Logger; 4 | import app.revanced.integrations.youtube.settings.Settings; 5 | 6 | @SuppressWarnings("unused") 7 | public class QUICProtocolPatch { 8 | 9 | public static boolean disableQUICProtocol(boolean original) { 10 | try { 11 | return !Settings.DISABLE_QUIC_PROTOCOL.get() && original; 12 | } catch (Exception ex) { 13 | Logger.printException(() -> "Failed to load disableQUICProtocol", ex); 14 | } 15 | return original; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/utils/DrawableColorPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.utils; 2 | 3 | @SuppressWarnings("unused") 4 | public class DrawableColorPatch { 5 | private static final int[] DARK_VALUES = { 6 | -14606047 // comments box background 7 | }; 8 | 9 | public static int getColor(int originalValue) { 10 | if (anyEquals(originalValue, DARK_VALUES)) 11 | return -16777215; 12 | 13 | return originalValue; 14 | } 15 | 16 | private static boolean anyEquals(int value, int... of) { 17 | for (int v : of) if (value == v) return true; 18 | return false; 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/TrendingTodayShelfPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import app.revanced.integrations.reddit.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class TrendingTodayShelfPatch { 7 | 8 | public static boolean hideTrendingTodayShelf() { 9 | return Settings.HIDE_TRENDING_TODAY_SHELF.get(); 10 | } 11 | 12 | public static String removeTrendingLabel(String label) { 13 | return Settings.HIDE_TRENDING_TODAY_SHELF.get() && 14 | label != null && 15 | label.startsWith("Trending") 16 | ? "" 17 | : label; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/preference/TogglePreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.preference.SwitchPreference; 5 | 6 | import app.revanced.integrations.shared.settings.BooleanSetting; 7 | 8 | @SuppressWarnings("deprecation") 9 | public class TogglePreference extends SwitchPreference { 10 | public TogglePreference(Context context, String title, String summary, BooleanSetting setting) { 11 | super(context); 12 | this.setTitle(title); 13 | this.setSummary(summary); 14 | this.setKey(setting.key); 15 | this.setChecked(setting.get()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/PlayerControlsVisibilityHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.youtube.shared.PlayerControlsVisibility; 6 | 7 | @SuppressWarnings("unused") 8 | public class PlayerControlsVisibilityHookPatch { 9 | /** 10 | * Injection point. 11 | */ 12 | public static void setPlayerControlsVisibility(@Nullable Enum youTubePlayerControlsVisibility) { 13 | if (youTubePlayerControlsVisibility == null) return; 14 | 15 | PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name()); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /stub/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java: -------------------------------------------------------------------------------- 1 | package androidx.coordinatorlayout.widget; 2 | 3 | import android.content.Context; 4 | import android.view.ViewGroup; 5 | 6 | /** 7 | * "CompileOnly" class 8 | *

9 | * This class will not be included and "replaced" by the real package's class. 10 | */ 11 | public class CoordinatorLayout extends ViewGroup { 12 | public CoordinatorLayout(Context context) { 13 | super(context); 14 | } 15 | 16 | @Override 17 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 18 | } 19 | 20 | @Override 21 | public void setVisibility(int visibility) { 22 | super.setVisibility(visibility); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/discord_ping_on_release.yml: -------------------------------------------------------------------------------- 1 | name: Ping Discord on release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | notify-discord: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: sarisia/actions-status-discord@v1 12 | if: always() 13 | with: 14 | webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} 15 | username: ReVanced Extended 16 | color: 0xff5252 17 | nodetail: true 18 | notimestamp: true 19 | content: "<@&1271198160613539872>" 20 | title: "Integrations `${{ github.event.release.tag_name }}` has been released!" 21 | description: | 22 | Click [here](${{ github.event.release.html_url }}) to read the changelog. -------------------------------------------------------------------------------- /stub/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | alias(libs.plugins.android.library) 5 | } 6 | 7 | android { 8 | namespace = "app.revanced.integrations.stub" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | multiDexEnabled = false 13 | minSdk = 24 14 | } 15 | 16 | buildTypes { 17 | release { 18 | isMinifyEnabled = false 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android-optimize.txt"), 21 | "proguard-rules.pro", 22 | ) 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_17 27 | targetCompatibility = JavaVersion.VERSION_17 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/components/PlayerFlyoutMenuFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.components; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | import app.revanced.integrations.shared.patches.components.Filter; 5 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 6 | 7 | @SuppressWarnings("unused") 8 | public final class PlayerFlyoutMenuFilter extends Filter { 9 | 10 | public PlayerFlyoutMenuFilter() { 11 | addIdentifierCallbacks( 12 | new StringFilterGroup( 13 | Settings.HIDE_FLYOUT_MENU_3_COLUMN_COMPONENT, 14 | "music_highlight_menu_item_carousel.eml", 15 | "tile_button_carousel.eml" 16 | ) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stub/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/utils/Event.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.utils 2 | 3 | /** 4 | * generic event provider class 5 | */ 6 | class Event { 7 | private val eventListeners = mutableSetOf<(T) -> Unit>() 8 | 9 | operator fun plusAssign(observer: (T) -> Unit) { 10 | addObserver(observer) 11 | } 12 | 13 | fun addObserver(observer: (T) -> Unit) { 14 | eventListeners.add(observer) 15 | } 16 | 17 | operator fun minusAssign(observer: (T) -> Unit) { 18 | removeObserver(observer) 19 | } 20 | 21 | private fun removeObserver(observer: (T) -> Unit) { 22 | eventListeners.remove(observer) 23 | } 24 | 25 | operator fun invoke(value: T) { 26 | for (observer in eventListeners) 27 | observer.invoke(value) 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/preference/categories/ConditionalPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceCategory; 5 | import android.preference.PreferenceScreen; 6 | 7 | @SuppressWarnings("deprecation") 8 | public abstract class ConditionalPreferenceCategory extends PreferenceCategory { 9 | public ConditionalPreferenceCategory(Context context, PreferenceScreen screen) { 10 | super(context); 11 | 12 | if (getSettingsStatus()) { 13 | screen.addPreference(this); 14 | addPreferences(context); 15 | } 16 | } 17 | 18 | public abstract boolean getSettingsStatus(); 19 | 20 | public abstract void addPreferences(Context context); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/OpenLinksDirectlyPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc; 2 | 3 | import android.net.Uri; 4 | 5 | import java.util.Objects; 6 | 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | @SuppressWarnings("unused") 10 | public class OpenLinksDirectlyPatch { 11 | private static final String YOUTUBE_REDIRECT_PATH = "/redirect"; 12 | 13 | public static Uri enableBypassRedirect(String uri) { 14 | final Uri parsed = Uri.parse(uri); 15 | if (!Settings.ENABLE_OPEN_LINKS_DIRECTLY.get()) 16 | return parsed; 17 | 18 | if (Objects.equals(parsed.getPath(), YOUTUBE_REDIRECT_PATH)) { 19 | return Uri.parse(Uri.decode(parsed.getQueryParameter("q"))); 20 | } 21 | 22 | return parsed; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/misc/SwipeControlsOverlay.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.misc 2 | 3 | /** 4 | * Interface for all overlays for swipe controls 5 | */ 6 | interface SwipeControlsOverlay { 7 | /** 8 | * called when the currently set volume level was changed 9 | * 10 | * @param newVolume the new volume level 11 | * @param maximumVolume the maximum volume index 12 | */ 13 | fun onVolumeChanged(newVolume: Int, maximumVolume: Int) 14 | 15 | /** 16 | * called when the currently set screen brightness was changed 17 | * 18 | * @param brightness the new screen brightness, in percent (range 0.0 - 100.0) 19 | */ 20 | fun onBrightnessChanged(brightness: Double) 21 | 22 | /** 23 | * called when a new swipe- session has started 24 | */ 25 | fun onEnterSwipeSession() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/ToolBarPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import android.view.View; 4 | import android.widget.ImageView; 5 | 6 | import app.revanced.integrations.shared.utils.Logger; 7 | 8 | @SuppressWarnings("unused") 9 | public class ToolBarPatch { 10 | 11 | public static void hookToolBar(Enum buttonEnum, ImageView imageView) { 12 | final String enumString = buttonEnum.name(); 13 | if (enumString.isEmpty() || 14 | imageView == null || 15 | !(imageView.getParent() instanceof View view)) { 16 | return; 17 | } 18 | 19 | Logger.printDebug(() -> "enumString: " + enumString); 20 | 21 | hookToolBar(enumString, view); 22 | } 23 | 24 | private static void hookToolBar(String enumString, View parentView) { 25 | } 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/LottieAnimationViewPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import com.airbnb.lottie.LottieAnimationView; 4 | 5 | import app.revanced.integrations.shared.utils.Logger; 6 | 7 | public class LottieAnimationViewPatch { 8 | 9 | public static void setLottieAnimationRawResources(LottieAnimationView lottieAnimationView, int rawRes) { 10 | if (lottieAnimationView == null) { 11 | Logger.printDebug(() -> "View is null"); 12 | return; 13 | } 14 | if (rawRes == 0) { 15 | Logger.printDebug(() -> "Resource is not found"); 16 | return; 17 | } 18 | setAnimation(lottieAnimationView, rawRes); 19 | } 20 | 21 | @SuppressWarnings("unused") 22 | private static void setAnimation(LottieAnimationView lottieAnimationView, int rawRes) { 23 | // Rest of the implementation added by patch. 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "dev", 6 | "prerelease": true 7 | } 8 | ], 9 | "plugins": [ 10 | "@semantic-release/commit-analyzer", 11 | "@semantic-release/release-notes-generator", 12 | "@semantic-release/changelog", 13 | "gradle-semantic-release-plugin", 14 | [ 15 | "@semantic-release/git", 16 | { 17 | "assets": [ 18 | "CHANGELOG.md", 19 | "gradle.properties" 20 | ] 21 | } 22 | ], 23 | [ 24 | "@semantic-release/github", 25 | { 26 | "assets": [ 27 | { 28 | "path": "app/build/outputs/apk/release/revanced-integrations*" 29 | } 30 | ], 31 | successComment: false 32 | } 33 | ], 34 | [ 35 | "@saithodev/semantic-release-backmerge", 36 | { 37 | backmergeBranches: [{"from": "main", "to": "dev"}], 38 | clearWorkspace: true 39 | } 40 | ] 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/returnyoutubeusername/requests/ChannelRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.returnyoutubeusername.requests; 2 | 3 | import static app.revanced.integrations.shared.requests.Route.Method.GET; 4 | 5 | import java.io.IOException; 6 | import java.net.HttpURLConnection; 7 | 8 | import app.revanced.integrations.shared.requests.Requester; 9 | import app.revanced.integrations.shared.requests.Route; 10 | 11 | public class ChannelRoutes { 12 | public static final String YOUTUBEI_V3_GAPIS_URL = "https://www.googleapis.com/youtube/v3/"; 13 | 14 | public static final Route GET_CHANNEL_DETAILS = new Route(GET, "channels?part=snippet&forHandle={handle}&key={api_key}"); 15 | 16 | public ChannelRoutes() { 17 | } 18 | 19 | public static HttpURLConnection getChannelConnectionFromRoute(Route route, String... params) throws IOException { 20 | return Requester.getConnectionFromRoute(YOUTUBEI_V3_GAPIS_URL, route, params); 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/misc/SwipeControlsUtils.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.misc 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | import kotlin.math.roundToInt 6 | 7 | fun Float.clamp(min: Float, max: Float): Float { 8 | if (this < min) return min 9 | if (this > max) return max 10 | return this 11 | } 12 | 13 | fun Int.clamp(min: Int, max: Int): Int { 14 | if (this < min) return min 15 | if (this > max) return max 16 | return this 17 | } 18 | 19 | fun Int.applyDimension(context: Context, unit: Int): Int { 20 | return TypedValue.applyDimension( 21 | unit, 22 | this.toFloat(), 23 | context.resources.displayMetrics, 24 | ).roundToInt() 25 | } 26 | 27 | fun Float.applyDimension(context: Context, unit: Int): Double { 28 | return TypedValue.applyDimension( 29 | unit, 30 | this, 31 | context.resources.displayMetrics, 32 | ).toDouble() 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/spans/StringFilterGroup.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.spans; 2 | 3 | import app.revanced.integrations.shared.settings.BooleanSetting; 4 | 5 | public class StringFilterGroup extends FilterGroup { 6 | 7 | public StringFilterGroup(final BooleanSetting setting, final String... filters) { 8 | super(setting, filters); 9 | } 10 | 11 | @Override 12 | public FilterGroupResult check(final String string) { 13 | int matchedIndex = -1; 14 | if (isEnabled()) { 15 | for (String pattern : filters) { 16 | if (!string.isEmpty()) { 17 | final int indexOf = string.indexOf(pattern); 18 | if (indexOf >= 0) { 19 | matchedIndex = indexOf; 20 | break; 21 | } 22 | } 23 | } 24 | } 25 | return new FilterGroupResult(setting, matchedIndex); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/AlwaysRepeatPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import static app.revanced.integrations.youtube.utils.VideoUtils.pauseMedia; 4 | 5 | import app.revanced.integrations.shared.utils.Utils; 6 | import app.revanced.integrations.youtube.settings.Settings; 7 | import app.revanced.integrations.youtube.shared.VideoInformation; 8 | 9 | @SuppressWarnings("unused") 10 | public class AlwaysRepeatPatch extends Utils { 11 | 12 | /** 13 | * Injection point. 14 | * 15 | * @return video is repeated. 16 | */ 17 | public static boolean alwaysRepeat() { 18 | return alwaysRepeatEnabled() && VideoInformation.overrideVideoTime(0); 19 | } 20 | 21 | public static boolean alwaysRepeatEnabled() { 22 | final boolean alwaysRepeat = Settings.ALWAYS_REPEAT.get(); 23 | final boolean alwaysRepeatPause = Settings.ALWAYS_REPEAT_PAUSE.get(); 24 | 25 | if (alwaysRepeat && alwaysRepeatPause) pauseMedia(); 26 | return alwaysRepeat; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/OpenLinksDirectlyPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import android.net.Uri; 4 | 5 | import app.revanced.integrations.reddit.settings.Settings; 6 | import app.revanced.integrations.shared.utils.Logger; 7 | 8 | @SuppressWarnings("unused") 9 | public final class OpenLinksDirectlyPatch { 10 | 11 | /** 12 | * Parses the given Reddit redirect uri by extracting the redirect query. 13 | * 14 | * @param uri The Reddit redirect uri. 15 | * @return The redirect query. 16 | */ 17 | public static Uri parseRedirectUri(Uri uri) { 18 | try { 19 | if (Settings.OPEN_LINKS_DIRECTLY.get()) { 20 | final String parsedUri = uri.getQueryParameter("url"); 21 | if (parsedUri != null && !parsedUri.isEmpty()) 22 | return Uri.parse(parsedUri); 23 | } 24 | } catch (Exception e) { 25 | Logger.printException(() -> "Can not parse URL: " + uri, e); 26 | } 27 | return uri; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/components/PlayerComponentsFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.components; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | import app.revanced.integrations.shared.patches.components.Filter; 5 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 6 | 7 | @SuppressWarnings("unused") 8 | public final class PlayerComponentsFilter extends Filter { 9 | 10 | public PlayerComponentsFilter() { 11 | addIdentifierCallbacks( 12 | new StringFilterGroup( 13 | Settings.HIDE_COMMENT_CHANNEL_GUIDELINES, 14 | "channel_guidelines_entry_banner.eml", 15 | "community_guidelines.eml" 16 | ) 17 | ); 18 | addPathCallbacks( 19 | new StringFilterGroup( 20 | Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS, 21 | "|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|" 22 | ) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/CastButtonPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class CastButtonPatch { 9 | 10 | /** 11 | * The [Hide cast button] setting is separated into the [Hide cast button in player] setting and the [Hide cast button in toolbar] setting. 12 | * Always hide the cast button when both settings are true. 13 | *

14 | * These two settings belong to different patches, and since the default value for this setting is true, 15 | * it is essential to ensure that each patch is included to ensure independent operation. 16 | */ 17 | public static int hideCastButton(int original) { 18 | return Settings.HIDE_TOOLBAR_CAST_BUTTON.get() 19 | && PatchStatus.ToolBarComponents() 20 | && Settings.HIDE_PLAYER_CAST_BUTTON.get() 21 | && PatchStatus.PlayerButtons() 22 | ? View.GONE 23 | : original; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/components/StringFilterGroup.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.components; 2 | 3 | import app.revanced.integrations.shared.settings.BooleanSetting; 4 | 5 | public class StringFilterGroup extends FilterGroup { 6 | 7 | public StringFilterGroup(final BooleanSetting setting, final String... filters) { 8 | super(setting, filters); 9 | } 10 | 11 | @Override 12 | public FilterGroupResult check(final String string) { 13 | int matchedIndex = -1; 14 | int matchedLength = 0; 15 | if (isEnabled()) { 16 | for (String pattern : filters) { 17 | if (!string.isEmpty()) { 18 | final int indexOf = string.indexOf(pattern); 19 | if (indexOf >= 0) { 20 | matchedIndex = indexOf; 21 | matchedLength = pattern.length(); 22 | break; 23 | } 24 | } 25 | } 26 | } 27 | return new FilterGroupResult(setting, matchedIndex, matchedLength); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/utils/StringTrieSearch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | /** 6 | * Text pattern searching using a prefix tree (trie). 7 | */ 8 | public final class StringTrieSearch extends TrieSearch { 9 | 10 | private static final class StringTrieNode extends TrieNode { 11 | StringTrieNode() { 12 | super(); 13 | } 14 | 15 | StringTrieNode(char nodeCharacterValue) { 16 | super(nodeCharacterValue); 17 | } 18 | 19 | @Override 20 | TrieNode createNode(char nodeValue) { 21 | return new StringTrieNode(nodeValue); 22 | } 23 | 24 | @Override 25 | char getCharValue(String text, int index) { 26 | return text.charAt(index); 27 | } 28 | 29 | @Override 30 | int getTextLength(String text) { 31 | return text.length(); 32 | } 33 | } 34 | 35 | public StringTrieSearch(@NonNull String... patterns) { 36 | super(new StringTrieNode(), patterns); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/preference/HtmlPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings.preference; 2 | 3 | import static android.text.Html.FROM_HTML_MODE_COMPACT; 4 | 5 | import android.content.Context; 6 | import android.preference.Preference; 7 | import android.text.Html; 8 | import android.util.AttributeSet; 9 | 10 | /** 11 | * Allows using basic html for the summary text. 12 | */ 13 | @SuppressWarnings({"unused", "deprecation"}) 14 | public class HtmlPreference extends Preference { 15 | { 16 | setSummary(Html.fromHtml(getSummary().toString(), FROM_HTML_MODE_COMPACT)); 17 | } 18 | 19 | public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 20 | super(context, attrs, defStyleAttr, defStyleRes); 21 | } 22 | 23 | public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) { 24 | super(context, attrs, defStyleAttr); 25 | } 26 | 27 | public HtmlPreference(Context context, AttributeSet attrs) { 28 | super(context, attrs); 29 | } 30 | 31 | public HtmlPreference(Context context) { 32 | super(context); 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/components/AdsFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.components; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | import app.revanced.integrations.shared.patches.components.Filter; 5 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 6 | 7 | @SuppressWarnings("unused") 8 | public final class AdsFilter extends Filter { 9 | 10 | public AdsFilter() { 11 | final StringFilterGroup alertBannerPromo = new StringFilterGroup( 12 | Settings.HIDE_PROMOTION_ALERT_BANNER, 13 | "alert_banner_promo.eml" 14 | ); 15 | 16 | final StringFilterGroup paidPromotionLabel = new StringFilterGroup( 17 | Settings.HIDE_PAID_PROMOTION_LABEL, 18 | "music_paid_content_overlay.eml" 19 | ); 20 | 21 | addIdentifierCallbacks(alertBannerPromo, paidPromotionLabel); 22 | 23 | final StringFilterGroup statementBanner = new StringFilterGroup( 24 | Settings.HIDE_GENERAL_ADS, 25 | "statement_banner" 26 | ); 27 | 28 | addPathCallbacks(statementBanner); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/PlaylistIdPrefix.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | public enum PlaylistIdPrefix { 6 | /** 7 | * To check all available prefixes, 8 | * See this document. 9 | */ 10 | ALL_CONTENTS_WITH_TIME_DESCENDING("UU"), 11 | ALL_CONTENTS_WITH_POPULAR_DESCENDING("PU"), 12 | VIDEOS_ONLY_WITH_TIME_DESCENDING("UULF"), 13 | VIDEOS_ONLY_WITH_POPULAR_DESCENDING("UULP"), 14 | SHORTS_ONLY_WITH_TIME_DESCENDING("UUSH"), 15 | SHORTS_ONLY_WITH_POPULAR_DESCENDING("UUPS"), 16 | LIVESTREAMS_ONLY_WITH_TIME_DESCENDING("UULV"), 17 | LIVESTREAMS_ONLY_WITH_POPULAR_DESCENDING("UUPV"), 18 | ALL_MEMBERSHIPS_CONTENTS("UUMO"), 19 | MEMBERSHIPS_VIDEOS_ONLY("UUMF"), 20 | MEMBERSHIPS_SHORTS_ONLY("UUMS"), 21 | MEMBERSHIPS_LIVESTREAMS_ONLY("UUMV"); 22 | 23 | /** 24 | * Prefix of playlist id. 25 | */ 26 | @NonNull 27 | public final String prefixId; 28 | 29 | PlaylistIdPrefix(@NonNull String prefixId) { 30 | this.prefixId = prefixId; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/preference/WideListPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings.preference; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.preference.ListPreference; 6 | import android.util.AttributeSet; 7 | 8 | import app.revanced.integrations.shared.utils.Utils; 9 | 10 | @SuppressWarnings({"unused", "deprecation"}) 11 | public class WideListPreference extends ListPreference { 12 | 13 | public WideListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 14 | super(context, attrs, defStyleAttr, defStyleRes); 15 | } 16 | 17 | public WideListPreference(Context context, AttributeSet attrs, int defStyleAttr) { 18 | super(context, attrs, defStyleAttr); 19 | } 20 | 21 | public WideListPreference(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | } 24 | 25 | public WideListPreference(Context context) { 26 | super(context); 27 | } 28 | 29 | @Override 30 | protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { 31 | Utils.setEditTextDialogTheme(builder, true); 32 | super.onPrepareDialogBuilder(builder); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/video/PlaybackSpeedPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.video; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.str; 4 | import static app.revanced.integrations.shared.utils.Utils.showToastShort; 5 | 6 | import app.revanced.integrations.music.settings.Settings; 7 | import app.revanced.integrations.shared.utils.Logger; 8 | 9 | @SuppressWarnings("unused") 10 | public class PlaybackSpeedPatch { 11 | 12 | public static float getPlaybackSpeed(final float playbackSpeed) { 13 | try { 14 | return Settings.DEFAULT_PLAYBACK_SPEED.get(); 15 | } catch (Exception ex) { 16 | Logger.printException(() -> "Failed to getPlaybackSpeed", ex); 17 | } 18 | return playbackSpeed; 19 | } 20 | 21 | public static void userSelectedPlaybackSpeed(final float playbackSpeed) { 22 | if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) 23 | return; 24 | 25 | Settings.DEFAULT_PLAYBACK_SPEED.save(playbackSpeed); 26 | 27 | if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) 28 | return; 29 | 30 | showToastShort(str("revanced_remember_playback_speed_toast", playbackSpeed + "x")); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/OpenLinksExternallyPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | 7 | import app.revanced.integrations.reddit.settings.Settings; 8 | import app.revanced.integrations.shared.utils.Logger; 9 | 10 | @SuppressWarnings("unused") 11 | public class OpenLinksExternallyPatch { 12 | 13 | /** 14 | * Override 'CustomTabsIntent', in order to open links in the default browser. 15 | * Instead of doing CustomTabsActivity, 16 | * 17 | * @param activity The activity, to start an Intent. 18 | * @param uri The URL to be opened in the default browser. 19 | */ 20 | public static boolean openLinksExternally(Activity activity, Uri uri) { 21 | try { 22 | if (activity != null && uri != null && Settings.OPEN_LINKS_EXTERNALLY.get()) { 23 | Intent intent = new Intent(Intent.ACTION_VIEW); 24 | intent.setData(uri); 25 | activity.startActivity(intent); 26 | return true; 27 | } 28 | } catch (Exception e) { 29 | Logger.printException(() -> "Can not open URL: " + uri, e); 30 | } 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/VideoState.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared 2 | 3 | import app.revanced.integrations.shared.utils.Logger 4 | 5 | /** 6 | * VideoState playback state. 7 | */ 8 | enum class VideoState { 9 | NEW, 10 | PLAYING, 11 | PAUSED, 12 | RECOVERABLE_ERROR, 13 | UNRECOVERABLE_ERROR, 14 | ENDED; 15 | 16 | companion object { 17 | 18 | private val nameToVideoState = entries.associateBy { it.name } 19 | 20 | @JvmStatic 21 | fun setFromString(enumName: String) { 22 | val state = nameToVideoState[enumName] 23 | if (state == null) { 24 | Logger.printException { "Unknown VideoState encountered: $enumName" } 25 | } else if (current != state) { 26 | Logger.printDebug { "VideoState changed to: $state" } 27 | current = state 28 | } 29 | } 30 | 31 | /** 32 | * Depending on which hook this is called from, 33 | * this value may not be up to date with the actual playback state. 34 | */ 35 | @JvmStatic 36 | var current 37 | get() = currentVideoState 38 | private set(value) { 39 | currentVideoState = value 40 | } 41 | 42 | private var currentVideoState: VideoState? = null 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/utils/InitializationPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.utils; 2 | 3 | import static app.revanced.integrations.music.utils.RestartUtils.showRestartDialog; 4 | 5 | import android.app.Activity; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import app.revanced.integrations.music.utils.ExtendedUtils; 10 | import app.revanced.integrations.shared.settings.BaseSettings; 11 | import app.revanced.integrations.shared.utils.Utils; 12 | 13 | @SuppressWarnings("unused") 14 | public class InitializationPatch { 15 | 16 | /** 17 | * The new layout is not loaded normally when the app is first installed. 18 | * (Also reproduced on unPatched YouTube Music) 19 | *

20 | * To fix this, show the reboot dialog when the app is installed for the first time. 21 | */ 22 | public static void onCreate(@NonNull Activity mActivity) { 23 | if (BaseSettings.SETTINGS_INITIALIZED.get()) 24 | return; 25 | 26 | showRestartDialog(mActivity, "revanced_extended_restart_first_run", 3000); 27 | Utils.runOnMainThreadDelayed(() -> BaseSettings.SETTINGS_INITIALIZED.save(true), 3000); 28 | } 29 | 30 | public static void setDeviceInformation(@NonNull Activity mActivity) { 31 | ExtendedUtils.setApplicationLabel(); 32 | ExtendedUtils.setVersionName(); 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.returnyoutubedislike.requests; 2 | 3 | import static app.revanced.integrations.shared.requests.Route.Method.GET; 4 | import static app.revanced.integrations.shared.requests.Route.Method.POST; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | 9 | import app.revanced.integrations.shared.requests.Requester; 10 | import app.revanced.integrations.shared.requests.Route; 11 | 12 | public class ReturnYouTubeDislikeRoutes { 13 | public static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/"; 14 | 15 | public static final Route SEND_VOTE = new Route(POST, "interact/vote"); 16 | public static final Route CONFIRM_VOTE = new Route(POST, "interact/confirmVote"); 17 | public static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}"); 18 | public static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}"); 19 | public static final Route CONFIRM_REGISTRATION = new Route(POST, "puzzle/registration?userId={user_id}"); 20 | 21 | public ReturnYouTubeDislikeRoutes() { 22 | } 23 | 24 | public static HttpURLConnection getRYDConnectionFromRoute(Route route, String... params) throws IOException { 25 | return Requester.getConnectionFromRoute(RYD_API_URL, route, params); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/WatchHistoryPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc; 2 | 3 | import android.net.Uri; 4 | 5 | import app.revanced.integrations.shared.utils.Logger; 6 | import app.revanced.integrations.youtube.settings.Settings; 7 | 8 | @SuppressWarnings("unused") 9 | public final class WatchHistoryPatch { 10 | 11 | public enum WatchHistoryType { 12 | ORIGINAL, 13 | REPLACE, 14 | BLOCK 15 | } 16 | 17 | private static final Uri UNREACHABLE_HOST_URI = Uri.parse("https://127.0.0.0"); 18 | private static final String WWW_TRACKING_URL_AUTHORITY = "www.youtube.com"; 19 | 20 | public static Uri replaceTrackingUrl(Uri trackingUrl) { 21 | final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get(); 22 | if (watchHistoryType != WatchHistoryType.ORIGINAL) { 23 | try { 24 | if (watchHistoryType == WatchHistoryType.REPLACE) { 25 | return trackingUrl.buildUpon().authority(WWW_TRACKING_URL_AUTHORITY).build(); 26 | } else if (watchHistoryType == WatchHistoryType.BLOCK) { 27 | return UNREACHABLE_HOST_URI; 28 | } 29 | } catch (Exception ex) { 30 | Logger.printException(() -> "replaceTrackingUrl failure", ex); 31 | } 32 | } 33 | 34 | return trackingUrl; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/components/ShareSheetMenuFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.music.patches.misc.ShareSheetPatch; 6 | import app.revanced.integrations.music.settings.Settings; 7 | import app.revanced.integrations.shared.patches.components.Filter; 8 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 9 | 10 | /** 11 | * Abuse LithoFilter for {@link ShareSheetPatch}. 12 | */ 13 | public final class ShareSheetMenuFilter extends Filter { 14 | // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. 15 | public static volatile boolean isShareSheetMenuVisible; 16 | 17 | public ShareSheetMenuFilter() { 18 | addIdentifierCallbacks( 19 | new StringFilterGroup( 20 | Settings.CHANGE_SHARE_SHEET, 21 | "share_sheet_container.eml" 22 | ) 23 | ); 24 | } 25 | 26 | @Override 27 | public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, 28 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 29 | isShareSheetMenuVisible = true; 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/ShareSheetMenuFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.shared.patches.components.Filter; 6 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 7 | import app.revanced.integrations.youtube.patches.misc.ShareSheetPatch; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | /** 11 | * Abuse LithoFilter for {@link ShareSheetPatch}. 12 | */ 13 | public final class ShareSheetMenuFilter extends Filter { 14 | // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. 15 | public static volatile boolean isShareSheetMenuVisible; 16 | 17 | public ShareSheetMenuFilter() { 18 | addIdentifierCallbacks( 19 | new StringFilterGroup( 20 | Settings.CHANGE_SHARE_SHEET, 21 | "share_sheet_container.eml" 22 | ) 23 | ); 24 | } 25 | 26 | @Override 27 | public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, 28 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 29 | isShareSheetMenuVisible = true; 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/components/LayoutComponentsFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.components; 2 | 3 | import app.revanced.integrations.music.settings.Settings; 4 | import app.revanced.integrations.shared.patches.components.Filter; 5 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 6 | 7 | @SuppressWarnings("unused") 8 | public final class LayoutComponentsFilter extends Filter { 9 | 10 | public LayoutComponentsFilter() { 11 | 12 | final StringFilterGroup buttonShelf = new StringFilterGroup( 13 | Settings.HIDE_BUTTON_SHELF, 14 | "entry_point_button_shelf.eml" 15 | ); 16 | 17 | final StringFilterGroup carouselShelf = new StringFilterGroup( 18 | Settings.HIDE_CAROUSEL_SHELF, 19 | "music_grid_item_carousel.eml" 20 | ); 21 | 22 | final StringFilterGroup playlistCardShelf = new StringFilterGroup( 23 | Settings.HIDE_PLAYLIST_CARD_SHELF, 24 | "music_container_card_shelf.eml" 25 | ); 26 | 27 | final StringFilterGroup sampleShelf = new StringFilterGroup( 28 | Settings.HIDE_SAMPLE_SHELF, 29 | "immersive_card_shelf.eml" 30 | ); 31 | 32 | addIdentifierCallbacks( 33 | buttonShelf, 34 | carouselShelf, 35 | playlistCardShelf, 36 | sampleShelf 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/VideoQualityMenuFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.shared.patches.components.Filter; 6 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 7 | import app.revanced.integrations.youtube.patches.video.RestoreOldVideoQualityMenuPatch; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | /** 11 | * Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}. 12 | */ 13 | public final class VideoQualityMenuFilter extends Filter { 14 | // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. 15 | public static volatile boolean isVideoQualityMenuVisible; 16 | 17 | public VideoQualityMenuFilter() { 18 | addPathCallbacks( 19 | new StringFilterGroup( 20 | Settings.RESTORE_OLD_VIDEO_QUALITY_MENU, 21 | "quick_quality_sheet_content.eml-js" 22 | ) 23 | ); 24 | } 25 | 26 | @Override 27 | public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, 28 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 29 | isVideoQualityMenuVisible = true; 30 | 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/BottomSheetState.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared 2 | 3 | import app.revanced.integrations.shared.utils.Event 4 | import app.revanced.integrations.shared.utils.Logger 5 | 6 | /** 7 | * BottomSheetState bottom sheet state. 8 | */ 9 | enum class BottomSheetState { 10 | CLOSED, 11 | OPEN; 12 | 13 | companion object { 14 | 15 | @JvmStatic 16 | fun set(enum: BottomSheetState) { 17 | if (current != enum) { 18 | Logger.printDebug { "BottomSheetState changed to: ${enum.name}" } 19 | current = enum 20 | } 21 | } 22 | 23 | /** 24 | * The current bottom sheet state. 25 | */ 26 | @JvmStatic 27 | var current 28 | get() = currentBottomSheetState 29 | private set(value) { 30 | currentBottomSheetState = value 31 | onChange(currentBottomSheetState) 32 | } 33 | 34 | @Volatile // value is read/write from different threads 35 | private var currentBottomSheetState = CLOSED 36 | 37 | /** 38 | * bottom sheet state change listener 39 | */ 40 | @JvmStatic 41 | val onChange = Event() 42 | } 43 | 44 | /** 45 | * Check if the bottom sheet is [OPEN]. 46 | * Useful for checking if a bottom sheet is open. 47 | */ 48 | fun isOpen(): Boolean { 49 | return this == OPEN 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/ShortsPlayerState.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared 2 | 3 | import app.revanced.integrations.shared.utils.Event 4 | import app.revanced.integrations.shared.utils.Logger 5 | 6 | /** 7 | * ShortsPlayerState shorts player state. 8 | */ 9 | enum class ShortsPlayerState { 10 | CLOSED, 11 | OPEN; 12 | 13 | companion object { 14 | 15 | @JvmStatic 16 | fun set(enum: ShortsPlayerState) { 17 | if (current != enum) { 18 | Logger.printDebug { "ShortsPlayerState changed to: ${enum.name}" } 19 | current = enum 20 | } 21 | } 22 | 23 | /** 24 | * The current shorts player state. 25 | */ 26 | @JvmStatic 27 | var current 28 | get() = currentShortsPlayerState 29 | private set(value) { 30 | currentShortsPlayerState = value 31 | onChange(value) 32 | } 33 | 34 | @Volatile // value is read/write from different threads 35 | private var currentShortsPlayerState = CLOSED 36 | 37 | /** 38 | * shorts player state change listener 39 | */ 40 | @JvmStatic 41 | val onChange = Event() 42 | } 43 | 44 | /** 45 | * Check if the shorts player is [CLOSED]. 46 | * Useful for checking if a shorts player is closed. 47 | */ 48 | fun isClosed(): Boolean { 49 | return this == CLOSED 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.preference.Preference; 7 | import android.util.AttributeSet; 8 | 9 | /** 10 | * Allows tapping the DeArrow about preference to open the DeArrow website. 11 | */ 12 | @SuppressWarnings({"unused", "deprecation"}) 13 | public class AlternativeThumbnailsAboutDeArrowPreference extends Preference { 14 | { 15 | setOnPreferenceClickListener(pref -> { 16 | Intent i = new Intent(Intent.ACTION_VIEW); 17 | i.setData(Uri.parse("https://dearrow.ajay.app")); 18 | pref.getContext().startActivity(i); 19 | return false; 20 | }); 21 | } 22 | 23 | public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 24 | super(context, attrs, defStyleAttr, defStyleRes); 25 | } 26 | 27 | public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) { 28 | super(context, attrs, defStyleAttr); 29 | } 30 | 31 | public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | } 34 | 35 | public AlternativeThumbnailsAboutDeArrowPreference(Context context) { 36 | super(context); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/ProgressBarDrawable.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import static app.revanced.integrations.youtube.patches.player.PlayerPatch.ORIGINAL_SEEKBAR_COLOR; 4 | import static app.revanced.integrations.youtube.patches.player.PlayerPatch.resumedProgressBarColor; 5 | 6 | import android.graphics.Canvas; 7 | import android.graphics.ColorFilter; 8 | import android.graphics.Paint; 9 | import android.graphics.PixelFormat; 10 | import android.graphics.drawable.Drawable; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.annotation.Nullable; 14 | 15 | import app.revanced.integrations.youtube.settings.Settings; 16 | 17 | @SuppressWarnings("unused") 18 | public class ProgressBarDrawable extends Drawable { 19 | 20 | private final Paint paint = new Paint(); 21 | 22 | @Override 23 | public void draw(@NonNull Canvas canvas) { 24 | if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { 25 | return; 26 | } 27 | paint.setColor(resumedProgressBarColor(ORIGINAL_SEEKBAR_COLOR)); 28 | canvas.drawRect(getBounds(), paint); 29 | } 30 | 31 | @Override 32 | public void setAlpha(int alpha) { 33 | paint.setAlpha(alpha); 34 | } 35 | 36 | @Override 37 | public void setColorFilter(@Nullable ColorFilter colorFilter) { 38 | paint.setColorFilter(colorFilter); 39 | } 40 | 41 | @Override 42 | public int getOpacity() { 43 | return PixelFormat.TRANSLUCENT; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/RootView.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared; 2 | 3 | import static app.revanced.integrations.youtube.patches.components.RelatedVideoFilter.isActionBarVisible; 4 | 5 | import android.view.View; 6 | 7 | import java.lang.ref.WeakReference; 8 | 9 | @SuppressWarnings("unused") 10 | public final class RootView { 11 | private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); 12 | 13 | /** 14 | * Injection point. 15 | */ 16 | public static void searchBarResultsViewLoaded(View searchbarResults) { 17 | searchBarResultsRef = new WeakReference<>(searchbarResults); 18 | } 19 | 20 | /** 21 | * @return If the search bar is on screen. This includes if the player 22 | * is on screen and the search results are behind the player (and not visible). 23 | * Detecting the search is covered by the player can be done by checking {@link RootView#isPlayerActive()}. 24 | */ 25 | public static boolean isSearchBarActive() { 26 | View searchbarResults = searchBarResultsRef.get(); 27 | return searchbarResults != null && searchbarResults.getParent() != null; 28 | } 29 | 30 | public static boolean isPlayerActive() { 31 | return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get(); 32 | } 33 | 34 | /** 35 | * Get current BrowseId. 36 | * Rest of the implementation added by patch. 37 | */ 38 | public static String getBrowseId() { 39 | return ""; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/ads/PremiumPromotionPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.ads; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.ImageView; 6 | import android.widget.LinearLayout; 7 | 8 | import app.revanced.integrations.music.settings.Settings; 9 | import app.revanced.integrations.shared.utils.Logger; 10 | 11 | @SuppressWarnings("unused") 12 | public class PremiumPromotionPatch { 13 | 14 | public static void hidePremiumPromotion(View view) { 15 | if (!Settings.HIDE_PREMIUM_PROMOTION.get()) 16 | return; 17 | 18 | view.getViewTreeObserver().addOnGlobalLayoutListener(() -> { 19 | try { 20 | if (!(view instanceof ViewGroup viewGroup)) { 21 | return; 22 | } 23 | if (!(viewGroup.getChildAt(0) instanceof ViewGroup mealBarLayoutRoot)) { 24 | return; 25 | } 26 | if (!(mealBarLayoutRoot.getChildAt(0) instanceof LinearLayout linearLayout)) { 27 | return; 28 | } 29 | if (!(linearLayout.getChildAt(0) instanceof ImageView imageView)) { 30 | return; 31 | } 32 | if (imageView.getVisibility() == View.VISIBLE) { 33 | view.setVisibility(View.GONE); 34 | } 35 | } catch (Exception ex) { 36 | Logger.printException(() -> "hideGetPremium failure", ex); 37 | } 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/utils/ByteTrieSearch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | 7 | public final class ByteTrieSearch extends TrieSearch { 8 | 9 | private static final class ByteTrieNode extends TrieNode { 10 | ByteTrieNode() { 11 | super(); 12 | } 13 | 14 | ByteTrieNode(char nodeCharacterValue) { 15 | super(nodeCharacterValue); 16 | } 17 | 18 | @Override 19 | TrieNode createNode(char nodeCharacterValue) { 20 | return new ByteTrieNode(nodeCharacterValue); 21 | } 22 | 23 | @Override 24 | char getCharValue(byte[] text, int index) { 25 | return (char) text[index]; 26 | } 27 | 28 | @Override 29 | int getTextLength(byte[] text) { 30 | return text.length; 31 | } 32 | } 33 | 34 | /** 35 | * Helper method for the common usage of converting Strings to raw UTF-8 bytes. 36 | */ 37 | public static byte[][] convertStringsToBytes(String... strings) { 38 | final int length = strings.length; 39 | byte[][] replacement = new byte[length][]; 40 | for (int i = 0; i < length; i++) { 41 | replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8); 42 | } 43 | return replacement; 44 | } 45 | 46 | public ByteTrieSearch(@NonNull byte[]... patterns) { 47 | super(new ByteTrieNode(), patterns); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/utils/RestartUtils.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.utils; 2 | 3 | import static app.revanced.integrations.music.utils.ExtendedUtils.getDialogBuilder; 4 | import static app.revanced.integrations.shared.utils.StringRef.str; 5 | import static app.revanced.integrations.shared.utils.Utils.runOnMainThreadDelayed; 6 | 7 | import android.app.Activity; 8 | import android.content.Intent; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import java.util.Objects; 13 | 14 | public class RestartUtils { 15 | 16 | public static void restartApp(@NonNull Activity activity) { 17 | final Intent intent = Objects.requireNonNull(activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName())); 18 | final Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent()); 19 | 20 | activity.finishAffinity(); 21 | activity.startActivity(mainIntent); 22 | Runtime.getRuntime().exit(0); 23 | } 24 | 25 | public static void showRestartDialog(@NonNull Activity activity) { 26 | showRestartDialog(activity, "revanced_extended_restart_message", 0); 27 | } 28 | 29 | public static void showRestartDialog(@NonNull Activity activity, @NonNull String message, long delay) { 30 | getDialogBuilder(activity) 31 | .setMessage(str(message)) 32 | .setPositiveButton(android.R.string.ok, (dialog, id) -> runOnMainThreadDelayed(() -> restartApp(activity), delay)) 33 | .setNegativeButton(android.R.string.cancel, null) 34 | .show(); 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/ads/AdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.ads; 2 | 3 | import static app.revanced.integrations.shared.utils.Utils.hideViewBy0dpUnderCondition; 4 | 5 | import android.view.View; 6 | 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | @SuppressWarnings("unused") 10 | public class AdsPatch { 11 | private static final boolean hideGeneralAdsEnabled = Settings.HIDE_GENERAL_ADS.get(); 12 | private static final boolean hideGetPremiumAdsEnabled = Settings.HIDE_GET_PREMIUM.get(); 13 | private static final boolean hideVideoAdsEnabled = Settings.HIDE_VIDEO_ADS.get(); 14 | 15 | /** 16 | * Injection point. 17 | * Hide the view, which shows ads in the homepage. 18 | * 19 | * @param view The view, which shows ads. 20 | */ 21 | public static void hideAdAttributionView(View view) { 22 | hideViewBy0dpUnderCondition(hideGeneralAdsEnabled, view); 23 | } 24 | 25 | public static boolean hideGetPremium() { 26 | return hideGetPremiumAdsEnabled; 27 | } 28 | 29 | /** 30 | * Injection point. 31 | */ 32 | public static boolean hideVideoAds() { 33 | return !hideVideoAdsEnabled; 34 | } 35 | 36 | /** 37 | * Injection point. 38 | *

39 | * Only used by old clients. 40 | * It is presumed to have been deprecated, and if it is confirmed that it is no longer used, remove it. 41 | */ 42 | public static boolean hideVideoAds(boolean original) { 43 | return !hideVideoAdsEnabled && original; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-issue.yml: -------------------------------------------------------------------------------- 1 | name: ⭐ Feature request 2 | description: Create a detailed feature request. 3 | title: 'feat: ' 4 | labels: [feature-request] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | # ReVanced feature request 10 | 11 | Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead. 12 | Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-integrations/labels/feature-request). 13 | 14 | - type: dropdown 15 | attributes: 16 | label: Type 17 | options: 18 | - Functionality 19 | - Cosmetic 20 | - Other 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Issue 26 | description: What is the current problem. Why does it require a feature request? 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: Feature 32 | description: Describe your feature in detail. How does it solve the issue? 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Motivation 38 | description: Why should your feature should be considered? 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Additional context 44 | description: Add additional context here. 45 | validations: 46 | required: false 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/sponsorblock/requests/SBRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.sponsorblock.requests; 2 | 3 | import static app.revanced.integrations.shared.requests.Route.Method.GET; 4 | import static app.revanced.integrations.shared.requests.Route.Method.POST; 5 | 6 | import app.revanced.integrations.shared.requests.Route; 7 | 8 | public class SBRoutes { 9 | public static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}"); 10 | public static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}"); 11 | public static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}"); 12 | public static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"ignoredSegmentCount\",\"viewCount\",\"minutesSaved\"]"); 13 | public static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}"); 14 | public static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?userID={user_id}&videoID={video_id}&category={category}&startTime={start_time}&endTime={end_time}&videoDuration={duration}"); 15 | public static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}"); 16 | public static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}"); 17 | 18 | public SBRoutes() { 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/AboutYouTubeDataAPIPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.preference.Preference; 6 | import android.util.AttributeSet; 7 | 8 | import app.revanced.integrations.shared.settings.preference.YouTubeDataAPIDialogBuilder; 9 | 10 | @SuppressWarnings({"unused", "deprecation"}) 11 | public class AboutYouTubeDataAPIPreference extends Preference implements Preference.OnPreferenceClickListener { 12 | 13 | private void init() { 14 | setSelectable(true); 15 | setOnPreferenceClickListener(this); 16 | } 17 | 18 | public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 19 | super(context, attrs, defStyleAttr, defStyleRes); 20 | init(); 21 | } 22 | 23 | public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr) { 24 | super(context, attrs, defStyleAttr); 25 | init(); 26 | } 27 | 28 | public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | init(); 31 | } 32 | 33 | public AboutYouTubeDataAPIPreference(Context context) { 34 | super(context); 35 | init(); 36 | } 37 | 38 | @Override 39 | public boolean onPreferenceClick(Preference preference) { 40 | if (getContext() instanceof Activity mActivity) { 41 | YouTubeDataAPIDialogBuilder.showDialog(mActivity); 42 | } 43 | 44 | return true; 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.shared.patches.components.Filter; 6 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | @SuppressWarnings("unused") 10 | public final class LayoutComponentsFilter extends Filter { 11 | private static final String ACCOUNT_HEADER_PATH = "account_header.eml"; 12 | 13 | public LayoutComponentsFilter() { 14 | addIdentifierCallbacks( 15 | new StringFilterGroup( 16 | Settings.HIDE_GRAY_SEPARATOR, 17 | "cell_divider" 18 | ) 19 | ); 20 | 21 | addPathCallbacks( 22 | new StringFilterGroup( 23 | Settings.HIDE_HANDLE, 24 | "|CellType|ContainerType|ContainerType|ContainerType|TextType|" 25 | ) 26 | ); 27 | } 28 | 29 | @Override 30 | public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, 31 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 32 | if (contentType == FilterContentType.PATH && !path.startsWith(ACCOUNT_HEADER_PATH)) { 33 | return false; 34 | } 35 | 36 | return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/preference/categories/AdsPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceScreen; 5 | 6 | import app.revanced.integrations.reddit.settings.Settings; 7 | import app.revanced.integrations.reddit.settings.SettingsStatus; 8 | import app.revanced.integrations.reddit.settings.preference.TogglePreference; 9 | 10 | @SuppressWarnings("deprecation") 11 | public class AdsPreferenceCategory extends ConditionalPreferenceCategory { 12 | public AdsPreferenceCategory(Context context, PreferenceScreen screen) { 13 | super(context, screen); 14 | setTitle("Ads"); 15 | } 16 | 17 | @Override 18 | public boolean getSettingsStatus() { 19 | return SettingsStatus.adsCategoryEnabled(); 20 | } 21 | 22 | @Override 23 | public void addPreferences(Context context) { 24 | addPreference(new TogglePreference( 25 | context, 26 | "Hide comment ads", 27 | "Hides ads in the comments section.", 28 | Settings.HIDE_COMMENT_ADS 29 | )); 30 | addPreference(new TogglePreference( 31 | context, 32 | "Hide feed ads", 33 | "Hides ads in the feed (old method).", 34 | Settings.HIDE_OLD_POST_ADS 35 | )); 36 | addPreference(new TogglePreference( 37 | context, 38 | "Hide feed ads", 39 | "Hides ads in the feed (new method).", 40 | Settings.HIDE_NEW_POST_ADS 41 | )); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/shared/PlayerType.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.shared 2 | 3 | import app.revanced.integrations.shared.utils.Event 4 | import app.revanced.integrations.shared.utils.Logger 5 | 6 | /** 7 | * WatchWhile player type 8 | */ 9 | enum class PlayerType { 10 | DISMISSED, 11 | MINIMIZED, 12 | MAXIMIZED_NOW_PLAYING, 13 | MAXIMIZED_PLAYER_ADDITIONAL_VIEW, 14 | FULLSCREEN, 15 | SLIDING_VERTICALLY, 16 | QUEUE_EXPANDING, 17 | SLIDING_HORIZONTALLY; 18 | 19 | companion object { 20 | 21 | private val nameToPlayerType = values().associateBy { it.name } 22 | 23 | @JvmStatic 24 | fun setFromString(enumName: String) { 25 | val newType = nameToPlayerType[enumName] 26 | if (newType == null) { 27 | Logger.printException { "Unknown PlayerType encountered: $enumName" } 28 | } else if (current != newType) { 29 | Logger.printDebug { "PlayerType changed to: $newType" } 30 | current = newType 31 | } 32 | } 33 | 34 | /** 35 | * The current player type. 36 | */ 37 | @JvmStatic 38 | var current 39 | get() = currentPlayerType 40 | private set(value) { 41 | currentPlayerType = value 42 | onChange(currentPlayerType) 43 | } 44 | 45 | @Volatile // value is read/write from different threads 46 | private var currentPlayerType = MINIMIZED 47 | 48 | /** 49 | * player type change listener 50 | */ 51 | @JvmStatic 52 | val onChange = Event<PlayerType>() 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/PlayerControlsVisibility.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared 2 | 3 | import app.revanced.integrations.shared.utils.Logger 4 | 5 | /** 6 | * PlayerControls visibility state. 7 | */ 8 | enum class PlayerControlsVisibility { 9 | PLAYER_CONTROLS_VISIBILITY_UNKNOWN, 10 | PLAYER_CONTROLS_VISIBILITY_WILL_HIDE, 11 | PLAYER_CONTROLS_VISIBILITY_HIDDEN, 12 | PLAYER_CONTROLS_VISIBILITY_WILL_SHOW, 13 | PLAYER_CONTROLS_VISIBILITY_SHOWN; 14 | 15 | companion object { 16 | 17 | private val nameToPlayerControlsVisibility = values().associateBy { it.name } 18 | 19 | @JvmStatic 20 | fun setFromString(enumName: String) { 21 | val state = nameToPlayerControlsVisibility[enumName] 22 | if (state == null) { 23 | Logger.printException { "Unknown PlayerControlsVisibility encountered: $enumName" } 24 | } else if (currentPlayerControlsVisibility != state) { 25 | Logger.printDebug { "PlayerControlsVisibility changed to: $state" } 26 | currentPlayerControlsVisibility = state 27 | } 28 | } 29 | 30 | /** 31 | * Depending on which hook this is called from, 32 | * this value may not be up to date with the actual playback state. 33 | */ 34 | @JvmStatic 35 | var current: PlayerControlsVisibility? 36 | get() = currentPlayerControlsVisibility 37 | private set(value) { 38 | currentPlayerControlsVisibility = value 39 | } 40 | 41 | private var currentPlayerControlsVisibility: PlayerControlsVisibility? = null 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/swipe/SwipeControlsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.swipe; 2 | 3 | import android.view.View; 4 | 5 | import java.lang.ref.WeakReference; 6 | 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | @SuppressWarnings({"unused", "deprecation"}) 10 | public class SwipeControlsPatch { 11 | private static WeakReference<View> fullscreenEngagementOverlayViewRef = new WeakReference<>(null); 12 | 13 | /** 14 | * Injection point. 15 | */ 16 | public static boolean disableHDRAutoBrightness() { 17 | return Settings.DISABLE_HDR_AUTO_BRIGHTNESS.get(); 18 | } 19 | 20 | /** 21 | * Injection point. 22 | */ 23 | public static boolean enableSwipeToSwitchVideo() { 24 | return Settings.ENABLE_SWIPE_TO_SWITCH_VIDEO.get(); 25 | } 26 | 27 | /** 28 | * Injection point. 29 | */ 30 | public static boolean enableWatchPanelGestures() { 31 | return Settings.ENABLE_WATCH_PANEL_GESTURES.get(); 32 | } 33 | 34 | /** 35 | * Injection point. 36 | * 37 | * @param fullscreenEngagementOverlayView R.layout.fullscreen_engagement_overlay 38 | */ 39 | public static void setFullscreenEngagementOverlayView(View fullscreenEngagementOverlayView) { 40 | fullscreenEngagementOverlayViewRef = new WeakReference<>(fullscreenEngagementOverlayView); 41 | } 42 | 43 | public static boolean isEngagementOverlayVisible() { 44 | final View engagementOverlayView = fullscreenEngagementOverlayViewRef.get(); 45 | return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/misc/ShareSheetPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.misc; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | import app.revanced.integrations.music.patches.components.ShareSheetMenuFilter; 8 | import app.revanced.integrations.music.settings.Settings; 9 | import app.revanced.integrations.shared.utils.Logger; 10 | import app.revanced.integrations.shared.utils.Utils; 11 | 12 | @SuppressWarnings("unused") 13 | public class ShareSheetPatch { 14 | /** 15 | * Injection point. 16 | */ 17 | public static void onShareSheetMenuCreate(final RecyclerView recyclerView) { 18 | if (!Settings.CHANGE_SHARE_SHEET.get()) 19 | return; 20 | 21 | recyclerView.getViewTreeObserver().addOnDrawListener(() -> { 22 | try { 23 | if (!ShareSheetMenuFilter.isShareSheetMenuVisible) 24 | return; 25 | if (!(recyclerView.getChildAt(0) instanceof ViewGroup shareContainer)) { 26 | return; 27 | } 28 | if (!(shareContainer.getChildAt(shareContainer.getChildCount() - 1) instanceof ViewGroup shareWithOtherAppsView)) { 29 | return; 30 | } 31 | ShareSheetMenuFilter.isShareSheetMenuVisible = false; 32 | 33 | recyclerView.setVisibility(View.GONE); 34 | Utils.clickView(shareWithOtherAppsView); 35 | } catch (Exception ex) { 36 | Logger.printException(() -> "onShareSheetMenuCreate failure", ex); 37 | } 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/GeneralAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.patches; 2 | 3 | import com.reddit.domain.model.ILink; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import app.revanced.integrations.reddit.settings.Settings; 10 | 11 | @SuppressWarnings("unused") 12 | public final class GeneralAdsPatch { 13 | 14 | private static List<?> filterChildren(final Iterable<?> links) { 15 | final List<Object> filteredList = new ArrayList<>(); 16 | 17 | for (Object item : links) { 18 | if (item instanceof ILink iLink && iLink.getPromoted()) continue; 19 | 20 | filteredList.add(item); 21 | } 22 | 23 | return filteredList; 24 | } 25 | 26 | public static boolean hideCommentAds() { 27 | return Settings.HIDE_COMMENT_ADS.get(); 28 | } 29 | 30 | public static Object hideCommentAdMap(Map<Object, Object> map, Object key, Object value) { 31 | if (!hideCommentAds()) { 32 | return map.put(key, value); 33 | } 34 | return map; 35 | } 36 | 37 | public static List<?> hideOldPostAds(List<?> list) { 38 | if (!Settings.HIDE_OLD_POST_ADS.get()) 39 | return list; 40 | 41 | return filterChildren(list); 42 | } 43 | 44 | public static List<?> hideNewPostAds(List<?> list) { 45 | return Settings.HIDE_NEW_POST_ADS.get() 46 | ? null 47 | : list; 48 | } 49 | 50 | public static void hideNewPostAds(ArrayList<Object> arrayList, Object object) { 51 | if (Settings.HIDE_NEW_POST_ADS.get()) 52 | return; 53 | 54 | arrayList.add(object); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/BypassImageRegionRestrictionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import app.revanced.integrations.shared.settings.BaseSettings; 6 | import app.revanced.integrations.shared.utils.Logger; 7 | 8 | @SuppressWarnings("unused") 9 | public final class BypassImageRegionRestrictionsPatch { 10 | 11 | private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BaseSettings.BYPASS_IMAGE_REGION_RESTRICTIONS.get(); 12 | private static final String REPLACEMENT_IMAGE_DOMAIN = BaseSettings.BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN.get(); 13 | 14 | /** 15 | * YouTube static images domain. Includes user and channel avatar images and community post images. 16 | */ 17 | private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN = Pattern.compile("(ap[1-2]|gm[1-4]|gz0|(cp|ci|gp|lh)[3-6]|sp[1-3]|yt[3-4]|(play|ccp)-lh)\\.(ggpht|googleusercontent)\\.com"); 18 | 19 | public static String overrideImageURL(String originalUrl) { 20 | try { 21 | if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) { 22 | final String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN 23 | .matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN); 24 | if (!replacement.equals(originalUrl)) { 25 | Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'"); 26 | } 27 | return replacement; 28 | } 29 | } catch (Exception ex) { 30 | Logger.printException(() -> "overrideImageURL failure", ex); 31 | } 32 | return originalUrl; 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | public class PatchStatus { 4 | 5 | public static boolean ImageSearchButton() { 6 | // Replace this with true if the Hide image search buttons patch succeeds 7 | return false; 8 | } 9 | 10 | public static boolean MinimalHeader() { 11 | // Replace this with true If the Custom header patch succeeds and the patch option was `youtube_minimal_header` 12 | return false; 13 | } 14 | 15 | public static boolean PlayerButtons() { 16 | // Replace this with true if the Hide player buttons patch succeeds 17 | return false; 18 | } 19 | 20 | public static boolean QuickActions() { 21 | // Replace this with true if the Fullscreen components patch succeeds 22 | return false; 23 | } 24 | 25 | public static boolean RememberPlaybackSpeed() { 26 | // Replace this with true if the Video playback patch succeeds 27 | return false; 28 | } 29 | 30 | public static boolean SponsorBlock() { 31 | // Replace this with true if the SponsorBlock patch succeeds 32 | return false; 33 | } 34 | 35 | public static boolean ToolBarComponents() { 36 | // Replace this with true if the Toolbar components patch succeeds 37 | return false; 38 | } 39 | 40 | // Modified by a patch. Do not touch. 41 | public static String RVXMusicPackageName() { 42 | return "com.google.android.apps.youtube.music"; 43 | } 44 | 45 | // Modified by a patch. Do not touch. 46 | public static boolean OldSeekbarThumbnailsDefaultBoolean() { 47 | return false; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/VolumeKeysController.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.controller 2 | 3 | import android.view.KeyEvent 4 | import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity 5 | 6 | /** 7 | * controller for custom volume button behaviour 8 | * 9 | * @param controller main controller instance 10 | */ 11 | class VolumeKeysController( 12 | private val controller: SwipeControlsHostActivity, 13 | ) { 14 | /** 15 | * key event handler 16 | * 17 | * @param event the key event 18 | * @return consume the event? 19 | */ 20 | fun onKeyEvent(event: KeyEvent): Boolean { 21 | if (!controller.config.overwriteVolumeKeyControls) { 22 | return false 23 | } 24 | 25 | return when (event.keyCode) { 26 | KeyEvent.KEYCODE_VOLUME_DOWN -> 27 | handleVolumeKeyEvent(event, false) 28 | 29 | KeyEvent.KEYCODE_VOLUME_UP -> 30 | handleVolumeKeyEvent(event, true) 31 | 32 | else -> false 33 | } 34 | } 35 | 36 | /** 37 | * handle a volume up / down key event 38 | * 39 | * @param event the key event 40 | * @param volumeUp was the key pressed the volume up key? 41 | * @return consume the event? 42 | */ 43 | private fun handleVolumeKeyEvent(event: KeyEvent, volumeUp: Boolean): Boolean { 44 | if (event.action == KeyEvent.ACTION_DOWN) { 45 | controller.audio?.apply { 46 | volume += if (volumeUp) 1 else -1 47 | controller.overlay.onVolumeChanged(volume, maxVolume) 48 | } 49 | } 50 | 51 | return true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/utils/IntentUtils.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.utils; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | public class IntentUtils extends Utils { 11 | 12 | public static void launchExternalDownloader(@NonNull String content, @NonNull String downloaderPackageName) { 13 | Intent intent = new Intent("android.intent.action.SEND"); 14 | intent.setType("text/plain"); 15 | intent.setPackage(downloaderPackageName); 16 | intent.putExtra("android.intent.extra.TEXT", content); 17 | launchIntent(intent); 18 | } 19 | 20 | private static void launchIntent(@NonNull Intent intent) { 21 | // If possible, use the main activity as the context. 22 | // Otherwise fall back on using the application context. 23 | Context mContext = getActivity(); 24 | if (mContext == null) { 25 | // Utils context is the application context, and not an activity context. 26 | mContext = context; 27 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 28 | } 29 | mContext.startActivity(intent); 30 | } 31 | 32 | public static void launchView(@NonNull String content) { 33 | launchView(content, null); 34 | } 35 | 36 | public static void launchView(@NonNull String content, @Nullable String packageName) { 37 | Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(content)); 38 | if (packageName != null) { 39 | intent.setPackage(packageName); 40 | } 41 | launchIntent(intent); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spans/SanitizeVideoSubtitleFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spans; 2 | 3 | import android.text.SpannableString; 4 | 5 | import app.revanced.integrations.shared.patches.spans.Filter; 6 | import app.revanced.integrations.shared.patches.spans.SpanType; 7 | import app.revanced.integrations.shared.patches.spans.StringFilterGroup; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | @SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"}) 11 | public final class SanitizeVideoSubtitleFilter extends Filter { 12 | 13 | public SanitizeVideoSubtitleFilter() { 14 | addCallbacks( 15 | new StringFilterGroup( 16 | Settings.SANITIZE_VIDEO_SUBTITLE, 17 | "|video_subtitle.eml|" 18 | ) 19 | ); 20 | } 21 | 22 | @Override 23 | public boolean skip(String conversionContext, SpannableString spannableString, Object span, 24 | int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) { 25 | if (isWord) { 26 | if (spanType == SpanType.IMAGE) { 27 | hideImageSpan(spannableString, start, end, flags); 28 | return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup); 29 | } else if (spanType == SpanType.CUSTOM_CHARACTER_STYLE) { 30 | hideSpan(spannableString, start, end, flags); 31 | return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup); 32 | } 33 | } 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/feed/RelatedVideoPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.feed; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import java.util.concurrent.atomic.AtomicBoolean; 6 | 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | import app.revanced.integrations.youtube.shared.BottomSheetState; 9 | import app.revanced.integrations.youtube.shared.RootView; 10 | 11 | @SuppressWarnings("unused") 12 | public final class RelatedVideoPatch { 13 | private static final boolean HIDE_RELATED_VIDEOS = Settings.HIDE_RELATED_VIDEOS.get(); 14 | 15 | private static final int OFFSET = Settings.RELATED_VIDEOS_OFFSET.get(); 16 | 17 | // video title,channel bar, video action bar, comment 18 | private static final int MAX_ITEM_COUNT = 4 + OFFSET; 19 | 20 | private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false); 21 | 22 | public static void showEngagementPanel(@Nullable Object object) { 23 | engagementPanelOpen.set(object != null); 24 | } 25 | 26 | public static void hideEngagementPanel() { 27 | engagementPanelOpen.compareAndSet(true, false); 28 | } 29 | 30 | public static int overrideItemCounts(int itemCounts) { 31 | if (!HIDE_RELATED_VIDEOS) { 32 | return itemCounts; 33 | } 34 | if (itemCounts < MAX_ITEM_COUNT) { 35 | return itemCounts; 36 | } 37 | if (!RootView.isPlayerActive()) { 38 | return itemCounts; 39 | } 40 | if (BottomSheetState.getCurrent().isOpen()) { 41 | return itemCounts; 42 | } 43 | if (engagementPanelOpen.get()) { 44 | return itemCounts; 45 | } 46 | return MAX_ITEM_COUNT; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/sponsorblock/SponsorBlockSettings.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.sponsorblock; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.UUID; 6 | 7 | import app.revanced.integrations.music.settings.Settings; 8 | import app.revanced.integrations.music.sponsorblock.objects.SegmentCategory; 9 | import app.revanced.integrations.shared.settings.Setting; 10 | 11 | public class SponsorBlockSettings { 12 | private static boolean initialized; 13 | 14 | /** 15 | * @return if the user has ever voted, created a segment, or imported existing SB settings. 16 | */ 17 | public static boolean userHasSBPrivateId() { 18 | return !Settings.SB_PRIVATE_USER_ID.get().isEmpty(); 19 | } 20 | 21 | /** 22 | * Use this only if a user id is required (creating segments, voting). 23 | */ 24 | @NonNull 25 | public static String getSBPrivateUserID() { 26 | String uuid = Settings.SB_PRIVATE_USER_ID.get(); 27 | if (uuid.isEmpty()) { 28 | uuid = (UUID.randomUUID().toString() + 29 | UUID.randomUUID().toString() + 30 | UUID.randomUUID().toString()) 31 | .replace("-", ""); 32 | Settings.SB_PRIVATE_USER_ID.save(uuid); 33 | } 34 | return uuid; 35 | } 36 | 37 | public static void initialize() { 38 | if (initialized) { 39 | return; 40 | } 41 | initialized = true; 42 | 43 | SegmentCategory.updateEnabledCategories(); 44 | } 45 | 46 | /** 47 | * Updates internal data based on {@link Setting} values. 48 | */ 49 | public static void updateFromImportedSettings() { 50 | SegmentCategory.loadAllCategoriesFromSettings(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/ads/PremiumRenewalPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.ads; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.str; 4 | 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.LinearLayout; 8 | import android.widget.TextView; 9 | 10 | import app.revanced.integrations.music.settings.Settings; 11 | import app.revanced.integrations.shared.utils.Logger; 12 | import app.revanced.integrations.shared.utils.Utils; 13 | 14 | @SuppressWarnings("unused") 15 | public class PremiumRenewalPatch { 16 | 17 | public static void hidePremiumRenewal(LinearLayout buttonContainerView) { 18 | if (!Settings.HIDE_PREMIUM_RENEWAL.get()) 19 | return; 20 | 21 | buttonContainerView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { 22 | try { 23 | Utils.runOnMainThreadDelayed(() -> { 24 | if (!(buttonContainerView.getChildAt(0) instanceof ViewGroup closeButtonParentView)) 25 | return; 26 | if (!(closeButtonParentView.getChildAt(0) instanceof TextView closeButtonView)) 27 | return; 28 | if (closeButtonView.getText().toString().equals(str("dialog_got_it_text"))) 29 | Utils.clickView(closeButtonView); 30 | else 31 | Utils.hideViewByLayoutParams((View) buttonContainerView.getParent()); 32 | }, 0 33 | ); 34 | } catch (Exception ex) { 35 | Logger.printException(() -> "hidePremiumRenewal failure", ex); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/InitializationPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import static app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog; 4 | import static app.revanced.integrations.shared.utils.StringRef.str; 5 | import static app.revanced.integrations.shared.utils.Utils.runOnMainThreadDelayed; 6 | 7 | import android.app.Activity; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import app.revanced.integrations.shared.settings.BaseSettings; 12 | import app.revanced.integrations.shared.settings.BooleanSetting; 13 | import app.revanced.integrations.youtube.utils.ExtendedUtils; 14 | 15 | @SuppressWarnings("unused") 16 | public class InitializationPatch { 17 | private static final BooleanSetting SETTINGS_INITIALIZED = BaseSettings.SETTINGS_INITIALIZED; 18 | 19 | /** 20 | * Some layouts that depend on litho do not load when the app is first installed. 21 | * (Also reproduced on unPatched YouTube) 22 | * <p> 23 | * To fix this, show the restart dialog when the app is installed for the first time. 24 | */ 25 | public static void onCreate(@NonNull Activity mActivity) { 26 | if (SETTINGS_INITIALIZED.get()) { 27 | return; 28 | } 29 | runOnMainThreadDelayed(() -> showRestartDialog(mActivity, str("revanced_extended_restart_first_run"), 3500), 500); 30 | runOnMainThreadDelayed(() -> SETTINGS_INITIALIZED.save(true), 1000); 31 | } 32 | 33 | public static void setExtendedUtils(@NonNull Activity mActivity) { 34 | ExtendedUtils.setApplicationLabel(); 35 | ExtendedUtils.setSmallestScreenWidthDp(); 36 | ExtendedUtils.setVersionName(); 37 | ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings(); 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/misc/ScrollDistanceHelper.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.misc 2 | 3 | import kotlin.math.abs 4 | import kotlin.math.sign 5 | 6 | /** 7 | * helper for scaling onScroll handler 8 | * 9 | * @param unitDistance absolute distance after which the callback is invoked 10 | * @param callback callback function for when unit distance is reached 11 | */ 12 | class ScrollDistanceHelper( 13 | private val unitDistance: Double, 14 | private val callback: (oldDistance: Double, newDistance: Double, direction: Int) -> Unit, 15 | ) { 16 | 17 | /** 18 | * total distance scrolled 19 | */ 20 | private var scrolledDistance: Double = 0.0 21 | 22 | /** 23 | * add a scrolled distance to the total. 24 | * if the [unitDistance] is reached, this function will also invoke the callback 25 | * 26 | * @param distance the distance to add 27 | */ 28 | fun add(distance: Double) { 29 | scrolledDistance += distance 30 | 31 | // invoke the callback if we scrolled far enough 32 | while (abs(scrolledDistance) >= unitDistance) { 33 | val oldDistance = scrolledDistance 34 | subtractUnitDistance() 35 | callback.invoke( 36 | oldDistance, 37 | scrolledDistance, 38 | sign(scrolledDistance).toInt(), 39 | ) 40 | } 41 | } 42 | 43 | /** 44 | * reset the distance scrolled to zero 45 | */ 46 | fun reset() { 47 | scrolledDistance = 0.0 48 | } 49 | 50 | /** 51 | * subtract the [unitDistance] from the total [scrolledDistance] 52 | */ 53 | private fun subtractUnitDistance() { 54 | scrolledDistance -= (unitDistance * sign(scrolledDistance)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/ExternalDownload.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.youtube.utils.VideoUtils; 11 | 12 | @SuppressWarnings("unused") 13 | public class ExternalDownload extends BottomControlButton { 14 | @Nullable 15 | private static ExternalDownload instance; 16 | 17 | public ExternalDownload(ViewGroup bottomControlsViewGroup) { 18 | super( 19 | bottomControlsViewGroup, 20 | "external_download_button", 21 | Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER, 22 | view -> VideoUtils.launchVideoExternalDownloader(), 23 | null 24 | ); 25 | } 26 | 27 | /** 28 | * Injection point. 29 | */ 30 | public static void initialize(View bottomControlsViewGroup) { 31 | try { 32 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 33 | instance = new ExternalDownload(viewGroup); 34 | } 35 | } catch (Exception ex) { 36 | Logger.printException(() -> "initialize failure", ex); 37 | } 38 | } 39 | 40 | /** 41 | * Injection point. 42 | */ 43 | public static void changeVisibility(boolean showing, boolean animation) { 44 | if (instance != null) instance.setVisibility(showing, animation); 45 | } 46 | 47 | public static void changeVisibilityNegatedImmediate() { 48 | if (instance != null) instance.setVisibilityNegatedImmediate(); 49 | } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/sponsorblock/objects/CategoryBehaviour.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.sponsorblock.objects; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.sf; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import java.util.Objects; 9 | 10 | import app.revanced.integrations.shared.utils.StringRef; 11 | 12 | public enum CategoryBehaviour { 13 | SKIP_AUTOMATICALLY("skip", 2, true, sf("revanced_sb_skip_automatically")), 14 | // ignored categories are not exported to json, and ignore is the default behavior when importing 15 | IGNORE("ignore", -1, false, sf("revanced_sb_skip_ignore")); 16 | 17 | /** 18 | * ReVanced specific value. 19 | */ 20 | @NonNull 21 | public final String reVancedKeyValue; 22 | /** 23 | * Desktop specific value. 24 | */ 25 | public final int desktopKeyValue; 26 | /** 27 | * If the segment should skip automatically 28 | */ 29 | public final boolean skipAutomatically; 30 | @NonNull 31 | public final StringRef description; 32 | 33 | CategoryBehaviour(String reVancedKeyValue, int desktopKeyValue, boolean skipAutomatically, StringRef description) { 34 | this.reVancedKeyValue = Objects.requireNonNull(reVancedKeyValue); 35 | this.desktopKeyValue = desktopKeyValue; 36 | this.skipAutomatically = skipAutomatically; 37 | this.description = Objects.requireNonNull(description); 38 | } 39 | 40 | @Nullable 41 | public static CategoryBehaviour byReVancedKeyValue(@NonNull String keyValue) { 42 | for (CategoryBehaviour behaviour : values()) { 43 | if (behaviour.reVancedKeyValue.equals(keyValue)) { 44 | return behaviour; 45 | } 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/PlayerTypeHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.Nullable; 6 | 7 | import app.revanced.integrations.youtube.shared.PlayerType; 8 | import app.revanced.integrations.youtube.shared.ShortsPlayerState; 9 | import app.revanced.integrations.youtube.shared.VideoState; 10 | 11 | @SuppressWarnings("unused") 12 | public class PlayerTypeHookPatch { 13 | /** 14 | * Injection point. 15 | */ 16 | public static void setPlayerType(@Nullable Enum<?> youTubePlayerType) { 17 | if (youTubePlayerType == null) return; 18 | 19 | PlayerType.setFromString(youTubePlayerType.name()); 20 | } 21 | 22 | /** 23 | * Injection point. 24 | */ 25 | public static void setVideoState(@Nullable Enum<?> youTubeVideoState) { 26 | if (youTubeVideoState == null) return; 27 | 28 | VideoState.setFromString(youTubeVideoState.name()); 29 | } 30 | 31 | /** 32 | * Injection point. 33 | * <p> 34 | * Add a listener to the shorts player overlay View. 35 | * Triggered when a shorts player is attached or detached to Windows. 36 | * 37 | * @param view shorts player overlay (R.id.reel_watch_player). 38 | */ 39 | public static void onShortsCreate(View view) { 40 | view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 41 | @Override 42 | public void onViewAttachedToWindow(@Nullable View v) { 43 | ShortsPlayerState.set(ShortsPlayerState.OPEN); 44 | } 45 | @Override 46 | public void onViewDetachedFromWindow(@Nullable View v) { 47 | ShortsPlayerState.set(ShortsPlayerState.CLOSED); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/utils/ExtendedUtils.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.utils; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.util.TypedValue; 6 | import android.view.ViewGroup; 7 | import android.widget.FrameLayout; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import app.revanced.integrations.music.settings.Settings; 12 | import app.revanced.integrations.shared.utils.PackageUtils; 13 | 14 | public class ExtendedUtils extends PackageUtils { 15 | 16 | public static boolean isSpoofingToLessThan(@NonNull String versionName) { 17 | if (!Settings.SPOOF_APP_VERSION.get()) 18 | return false; 19 | 20 | return isVersionToLessThan(Settings.SPOOF_APP_VERSION_TARGET.get(), versionName); 21 | } 22 | 23 | private static int dpToPx(float dp) { 24 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); 25 | } 26 | 27 | @SuppressWarnings("deprecation") 28 | public static AlertDialog.Builder getDialogBuilder(@NonNull Context context) { 29 | return new AlertDialog.Builder(context, isSDKAbove(22) 30 | ? android.R.style.Theme_DeviceDefault_Dialog_Alert 31 | : AlertDialog.THEME_DEVICE_DEFAULT_DARK 32 | ); 33 | } 34 | 35 | public static FrameLayout.LayoutParams getLayoutParams() { 36 | FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 37 | 38 | int left_margin = dpToPx(20); 39 | int top_margin = dpToPx(10); 40 | int right_margin = dpToPx(20); 41 | int bottom_margin = dpToPx(4); 42 | params.setMargins(left_margin, top_margin, right_margin, bottom_margin); 43 | 44 | return params; 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/DrawableColorPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import app.revanced.integrations.shared.utils.ResourceUtils; 4 | 5 | @SuppressWarnings("unused") 6 | public class DrawableColorPatch { 7 | private static final int[] WHITE_VALUES = { 8 | -1, // comments chip background 9 | -394759, // music related results panel background 10 | -83886081 // video chapters list background 11 | }; 12 | 13 | private static final int[] DARK_VALUES = { 14 | -14145496, // drawer content view background 15 | -14606047, // comments chip background 16 | -15198184, // music related results panel background 17 | -15790321, // comments chip background (new layout) 18 | -98492127 // video chapters list background 19 | }; 20 | 21 | // background colors 22 | private static int whiteColor = 0; 23 | private static int blackColor = 0; 24 | 25 | public static int getColor(int originalValue) { 26 | if (anyEquals(originalValue, DARK_VALUES)) { 27 | return getBlackColor(); 28 | } else if (anyEquals(originalValue, WHITE_VALUES)) { 29 | return getWhiteColor(); 30 | } 31 | return originalValue; 32 | } 33 | 34 | private static int getBlackColor() { 35 | if (blackColor == 0) blackColor = ResourceUtils.getColor("yt_black1"); 36 | return blackColor; 37 | } 38 | 39 | private static int getWhiteColor() { 40 | if (whiteColor == 0) whiteColor = ResourceUtils.getColor("yt_white1"); 41 | return whiteColor; 42 | } 43 | 44 | private static boolean anyEquals(int value, int... of) { 45 | for (int v : of) if (value == v) return true; 46 | return false; 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/CopyVideoUrl.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.youtube.utils.VideoUtils; 11 | 12 | @SuppressWarnings("unused") 13 | public class CopyVideoUrl extends BottomControlButton { 14 | @Nullable 15 | private static CopyVideoUrl instance; 16 | 17 | public CopyVideoUrl(ViewGroup bottomControlsViewGroup) { 18 | super( 19 | bottomControlsViewGroup, 20 | "copy_video_url_button", 21 | Settings.OVERLAY_BUTTON_COPY_VIDEO_URL, 22 | view -> VideoUtils.copyUrl(false), 23 | view -> { 24 | VideoUtils.copyUrl(true); 25 | return true; 26 | } 27 | ); 28 | } 29 | 30 | /** 31 | * Injection point. 32 | */ 33 | public static void initialize(View bottomControlsViewGroup) { 34 | try { 35 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 36 | instance = new CopyVideoUrl(viewGroup); 37 | } 38 | } catch (Exception ex) { 39 | Logger.printException(() -> "initialize failure", ex); 40 | } 41 | } 42 | 43 | /** 44 | * Injection point. 45 | */ 46 | public static void changeVisibility(boolean showing, boolean animation) { 47 | if (instance != null) instance.setVisibility(showing, animation); 48 | } 49 | 50 | public static void changeVisibilityNegatedImmediate() { 51 | if (instance != null) instance.setVisibilityNegatedImmediate(); 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/PlayAll.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.youtube.utils.VideoUtils; 11 | 12 | @SuppressWarnings("unused") 13 | public class PlayAll extends BottomControlButton { 14 | 15 | @Nullable 16 | private static PlayAll instance; 17 | 18 | public PlayAll(ViewGroup bottomControlsViewGroup) { 19 | super( 20 | bottomControlsViewGroup, 21 | "play_all_button", 22 | Settings.OVERLAY_BUTTON_PLAY_ALL, 23 | view -> VideoUtils.openVideo(Settings.OVERLAY_BUTTON_PLAY_ALL_TYPE.get()), 24 | view -> { 25 | VideoUtils.openVideo(); 26 | return true; 27 | } 28 | ); 29 | } 30 | 31 | /** 32 | * Injection point. 33 | */ 34 | public static void initialize(View bottomControlsViewGroup) { 35 | try { 36 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 37 | instance = new PlayAll(viewGroup); 38 | } 39 | } catch (Exception ex) { 40 | Logger.printException(() -> "initialize failure", ex); 41 | } 42 | } 43 | 44 | /** 45 | * Injection point. 46 | */ 47 | public static void changeVisibility(boolean showing, boolean animation) { 48 | if (instance != null) instance.setVisibility(showing, animation); 49 | } 50 | 51 | public static void changeVisibilityNegatedImmediate() { 52 | if (instance != null) instance.setVisibilityNegatedImmediate(); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/LockModeState.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared 2 | 3 | import app.revanced.integrations.shared.utils.Event 4 | import app.revanced.integrations.shared.utils.Logger 5 | 6 | /** 7 | * LockModeState. 8 | */ 9 | enum class LockModeState { 10 | LOCK_MODE_STATE_ENUM_UNKNOWN, 11 | LOCK_MODE_STATE_ENUM_UNLOCKED, 12 | LOCK_MODE_STATE_ENUM_LOCKED, 13 | LOCK_MODE_STATE_ENUM_CAN_UNLOCK, 14 | LOCK_MODE_STATE_ENUM_UNLOCK_EXPANDED, 15 | LOCK_MODE_STATE_ENUM_LOCKED_TEMPORARY_SUSPENSION; 16 | 17 | companion object { 18 | 19 | private val nameToLockModeState = entries.associateBy { it.name } 20 | 21 | @JvmStatic 22 | fun setFromString(enumName: String) { 23 | val newType = nameToLockModeState[enumName] 24 | if (newType == null) { 25 | Logger.printException { "Unknown LockModeState encountered: $enumName" } 26 | } else if (current != newType) { 27 | Logger.printDebug { "LockModeState changed to: $newType" } 28 | current = newType 29 | } 30 | } 31 | 32 | /** 33 | * The current lock mode state. 34 | */ 35 | @JvmStatic 36 | var current 37 | get() = currentLockModeState 38 | private set(value) { 39 | currentLockModeState = value 40 | onChange(value) 41 | } 42 | 43 | @Volatile // value is read/write from different threads 44 | private var currentLockModeState = LOCK_MODE_STATE_ENUM_UNKNOWN 45 | 46 | /** 47 | * player type change listener 48 | */ 49 | @JvmStatic 50 | val onChange = Event<LockModeState>() 51 | } 52 | 53 | fun isLocked(): Boolean { 54 | return this == LOCK_MODE_STATE_ENUM_LOCKED || this == LOCK_MODE_STATE_ENUM_UNLOCK_EXPANDED 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/CopyVideoUrlTimestamp.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.youtube.utils.VideoUtils; 11 | 12 | @SuppressWarnings("unused") 13 | public class CopyVideoUrlTimestamp extends BottomControlButton { 14 | @Nullable 15 | private static CopyVideoUrlTimestamp instance; 16 | 17 | public CopyVideoUrlTimestamp(ViewGroup bottomControlsViewGroup) { 18 | super( 19 | bottomControlsViewGroup, 20 | "copy_video_url_timestamp_button", 21 | Settings.OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP, 22 | view -> VideoUtils.copyUrl(true), 23 | view -> { 24 | VideoUtils.copyTimeStamp(); 25 | return true; 26 | } 27 | ); 28 | } 29 | 30 | /** 31 | * Injection point. 32 | */ 33 | public static void initialize(View bottomControlsViewGroup) { 34 | try { 35 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 36 | instance = new CopyVideoUrlTimestamp(viewGroup); 37 | } 38 | } catch (Exception ex) { 39 | Logger.printException(() -> "initialize failure", ex); 40 | } 41 | } 42 | 43 | /** 44 | * Injection point. 45 | */ 46 | public static void changeVisibility(boolean showing, boolean animation) { 47 | if (instance != null) instance.setVisibility(showing, animation); 48 | } 49 | 50 | public static void changeVisibilityNegatedImmediate() { 51 | if (instance != null) instance.setVisibilityNegatedImmediate(); 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/UserStats.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.sponsorblock.objects; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | /** 9 | * SponsorBlock user stats 10 | */ 11 | public class UserStats { 12 | @NonNull 13 | public final String publicUserId; 14 | @NonNull 15 | public final String userName; 16 | /** 17 | * "User reputation". Unclear how SB determines this value. 18 | */ 19 | public final float reputation; 20 | /** 21 | * {@link #segmentCount} plus {@link #ignoredSegmentCount} 22 | */ 23 | public final int totalSegmentCountIncludingIgnored; 24 | public final int segmentCount; 25 | public final int ignoredSegmentCount; 26 | public final int viewCount; 27 | public final double minutesSaved; 28 | 29 | public UserStats(@NonNull JSONObject json) throws JSONException { 30 | publicUserId = json.getString("userID"); 31 | userName = json.getString("userName"); 32 | reputation = (float) json.getDouble("reputation"); 33 | segmentCount = json.getInt("segmentCount"); 34 | ignoredSegmentCount = json.getInt("ignoredSegmentCount"); 35 | totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount; 36 | viewCount = json.getInt("viewCount"); 37 | minutesSaved = json.getDouble("minutesSaved"); 38 | } 39 | 40 | @NonNull 41 | @Override 42 | public String toString() { 43 | return "UserStats{" 44 | + "publicUserId='" + publicUserId + '\'' 45 | + ", userName='" + userName + '\'' 46 | + ", reputation=" + reputation 47 | + ", segmentCount=" + segmentCount 48 | + ", ignoredSegmentCount=" + ignoredSegmentCount 49 | + ", viewCount=" + viewCount 50 | + ", minutesSaved=" + minutesSaved 51 | + '}'; 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/shared/VideoType.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.shared 2 | 3 | import app.revanced.integrations.shared.utils.Event 4 | import app.revanced.integrations.shared.utils.Logger 5 | 6 | /** 7 | * Music video type 8 | */ 9 | enum class VideoType { 10 | MUSIC_VIDEO_TYPE_UNKNOWN, 11 | MUSIC_VIDEO_TYPE_ATV, 12 | MUSIC_VIDEO_TYPE_OMV, 13 | MUSIC_VIDEO_TYPE_UGC, 14 | MUSIC_VIDEO_TYPE_SHOULDER, 15 | MUSIC_VIDEO_TYPE_OFFICIAL_SOURCE_MUSIC, 16 | MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK, 17 | MUSIC_VIDEO_TYPE_LIVE_STREAM, 18 | MUSIC_VIDEO_TYPE_PODCAST_EPISODE; 19 | 20 | companion object { 21 | 22 | private val nameToVideoType = values().associateBy { it.name } 23 | 24 | @JvmStatic 25 | fun setFromString(enumName: String) { 26 | val newType = nameToVideoType[enumName] 27 | if (newType == null) { 28 | Logger.printException { "Unknown VideoType encountered: $enumName" } 29 | } else if (current != newType) { 30 | Logger.printDebug { "VideoType changed to: $newType" } 31 | current = newType 32 | } 33 | } 34 | 35 | /** 36 | * The current video type. 37 | */ 38 | @JvmStatic 39 | var current 40 | get() = currentVideoType 41 | private set(value) { 42 | currentVideoType = value 43 | onChange(currentVideoType) 44 | } 45 | 46 | @Volatile // value is read/write from different threads 47 | private var currentVideoType = MUSIC_VIDEO_TYPE_UNKNOWN 48 | 49 | /** 50 | * player type change listener 51 | */ 52 | @JvmStatic 53 | val onChange = Event<VideoType>() 54 | } 55 | 56 | fun isMusicVideo(): Boolean { 57 | return this == MUSIC_VIDEO_TYPE_OMV 58 | } 59 | 60 | fun isPodCast(): Boolean { 61 | return this == MUSIC_VIDEO_TYPE_PODCAST_EPISODE 62 | } 63 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-issue.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report a very clearly broken issue. 3 | title: 'bug: <title>' 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | # ReVanced bug report 10 | 11 | Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-integrations/labels/bug). 12 | 13 | - type: dropdown 14 | attributes: 15 | label: Type 16 | options: 17 | - Cosmetic 18 | - Other 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Bug description 24 | description: How did you find the bug? Any additional details that might help? 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Steps to reproduce 30 | description: Add the steps to reproduce this bug including your environment. 31 | placeholder: Step 1. Download some files. Step 2. ... 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Relevant log output 37 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 38 | render: shell 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Screenshots or videos 44 | description: Add screenshots or videos that show the bug here. 45 | placeholder: Drag and drop the screenshots/videos into this box. 46 | validations: 47 | required: false 48 | - type: textarea 49 | attributes: 50 | label: Solution 51 | description: If applicable, add a possible solution. 52 | validations: 53 | required: false 54 | - type: textarea 55 | attributes: 56 | label: Additional context 57 | description: Add additional context here. 58 | validations: 59 | required: false 60 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/OpenDefaultAppSettingsPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import static app.revanced.integrations.shared.utils.Utils.isSDKAbove; 4 | 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import android.preference.Preference; 9 | import android.util.AttributeSet; 10 | 11 | import app.revanced.integrations.shared.utils.Logger; 12 | import app.revanced.integrations.shared.utils.Utils; 13 | 14 | @SuppressWarnings({"unused", "deprecation"}) 15 | public class OpenDefaultAppSettingsPreference extends Preference { 16 | { 17 | setOnPreferenceClickListener(pref -> { 18 | try { 19 | Context context = Utils.getActivity(); 20 | final Uri uri = Uri.parse("package:" + context.getPackageName()); 21 | final Intent intent = isSDKAbove(31) 22 | ? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri) 23 | : new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri); 24 | context.startActivity(intent); 25 | } catch (Exception exception) { 26 | Logger.printException(() -> "OpenDefaultAppSettings Failed"); 27 | } 28 | return false; 29 | }); 30 | } 31 | 32 | public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 33 | super(context, attrs, defStyleAttr, defStyleRes); 34 | } 35 | 36 | public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr) { 37 | super(context, attrs, defStyleAttr); 38 | } 39 | 40 | public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | } 43 | 44 | public OpenDefaultAppSettingsPreference(Context context) { 45 | super(context); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/Whitelists.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.youtube.settings.preference.WhitelistedChannelsPreference; 11 | import app.revanced.integrations.youtube.whitelist.Whitelist; 12 | 13 | @SuppressWarnings("unused") 14 | public class Whitelists extends BottomControlButton { 15 | @Nullable 16 | private static Whitelists instance; 17 | 18 | public Whitelists(ViewGroup bottomControlsViewGroup) { 19 | super( 20 | bottomControlsViewGroup, 21 | "whitelist_button", 22 | Settings.OVERLAY_BUTTON_WHITELIST, 23 | view -> Whitelist.showWhitelistDialog(view.getContext()), 24 | view -> { 25 | WhitelistedChannelsPreference.showWhitelistedChannelDialog(view.getContext()); 26 | return true; 27 | } 28 | ); 29 | } 30 | 31 | /** 32 | * Injection point. 33 | */ 34 | public static void initialize(View bottomControlsViewGroup) { 35 | try { 36 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 37 | instance = new Whitelists(viewGroup); 38 | } 39 | } catch (Exception ex) { 40 | Logger.printException(() -> "initialize failure", ex); 41 | } 42 | } 43 | 44 | /** 45 | * Injection point. 46 | */ 47 | public static void changeVisibility(boolean showing, boolean animation) { 48 | if (instance != null) instance.setVisibility(showing, animation); 49 | } 50 | 51 | public static void changeVisibilityNegatedImmediate() { 52 | if (instance != null) instance.setVisibilityNegatedImmediate(); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/AlwaysRepeat.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | 11 | @SuppressWarnings("unused") 12 | public class AlwaysRepeat extends BottomControlButton { 13 | @Nullable 14 | private static AlwaysRepeat instance; 15 | 16 | public AlwaysRepeat(ViewGroup bottomControlsViewGroup) { 17 | super( 18 | bottomControlsViewGroup, 19 | "always_repeat_button", 20 | Settings.OVERLAY_BUTTON_ALWAYS_REPEAT, 21 | Settings.ALWAYS_REPEAT, 22 | Settings.ALWAYS_REPEAT_PAUSE, 23 | view -> { 24 | if (instance != null) 25 | instance.changeSelected(!view.isSelected()); 26 | }, 27 | view -> { 28 | if (instance != null) 29 | instance.changeColorFilter(); 30 | return true; 31 | } 32 | ); 33 | } 34 | 35 | /** 36 | * Injection point. 37 | */ 38 | public static void initialize(View bottomControlsViewGroup) { 39 | try { 40 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 41 | instance = new AlwaysRepeat(viewGroup); 42 | } 43 | } catch (Exception ex) { 44 | Logger.printException(() -> "initialize failure", ex); 45 | } 46 | } 47 | 48 | /** 49 | * Injection point. 50 | */ 51 | public static void changeVisibility(boolean showing, boolean animation) { 52 | if (instance != null) instance.setVisibility(showing, animation); 53 | } 54 | 55 | public static void changeVisibilityNegatedImmediate() { 56 | if (instance != null) instance.setVisibilityNegatedImmediate(); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/preference/categories/MiscellaneousPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceScreen; 5 | 6 | import app.revanced.integrations.reddit.settings.Settings; 7 | import app.revanced.integrations.reddit.settings.SettingsStatus; 8 | import app.revanced.integrations.reddit.settings.preference.TogglePreference; 9 | 10 | @SuppressWarnings("deprecation") 11 | public class MiscellaneousPreferenceCategory extends ConditionalPreferenceCategory { 12 | public MiscellaneousPreferenceCategory(Context context, PreferenceScreen screen) { 13 | super(context, screen); 14 | setTitle("Miscellaneous"); 15 | } 16 | 17 | @Override 18 | public boolean getSettingsStatus() { 19 | return SettingsStatus.miscellaneousCategoryEnabled(); 20 | } 21 | 22 | @Override 23 | public void addPreferences(Context context) { 24 | if (SettingsStatus.openLinksDirectlyEnabled) { 25 | addPreference(new TogglePreference( 26 | context, 27 | "Open links directly", 28 | "Skips over redirection URLs in external links.", 29 | Settings.OPEN_LINKS_DIRECTLY 30 | )); 31 | } 32 | if (SettingsStatus.openLinksExternallyEnabled) { 33 | addPreference(new TogglePreference( 34 | context, 35 | "Open links externally", 36 | "Opens links in your browser instead of in the in-app-browser.", 37 | Settings.OPEN_LINKS_EXTERNALLY 38 | )); 39 | } 40 | if (SettingsStatus.sanitizeUrlQueryEnabled) { 41 | addPreference(new TogglePreference( 42 | context, 43 | "Sanitize sharing links", 44 | "Sanitizes sharing links by removing tracking query parameters.", 45 | Settings.SANITIZE_URL_QUERY 46 | )); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/video/AV1CodecPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.video; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.str; 4 | 5 | import app.revanced.integrations.shared.utils.Logger; 6 | import app.revanced.integrations.shared.utils.Utils; 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | @SuppressWarnings("unused") 10 | public class AV1CodecPatch { 11 | private static final int LITERAL_VALUE_AV01 = 1635135811; 12 | private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123; 13 | private static final String VP9_CODEC = "video/x-vnd.on2.vp9"; 14 | private static long lastTimeResponse = 0; 15 | 16 | /** 17 | * Replace the SW AV01 codec to VP9 codec. 18 | * May not be valid on some clients. 19 | * 20 | * @param original hardcoded value - "video/av01" 21 | */ 22 | public static String replaceCodec(String original) { 23 | return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original; 24 | } 25 | 26 | /** 27 | * Replace the SW AV01 codec request with a Dolby Vision codec request. 28 | * This request is invalid, so it falls back to codecs other than AV01. 29 | * <p> 30 | * Limitation: Fallback process causes about 15-20 seconds of buffering. 31 | * 32 | * @param literalValue literal value of the codec 33 | */ 34 | public static int rejectResponse(int literalValue) { 35 | if (!Settings.REJECT_AV1_CODEC.get()) 36 | return literalValue; 37 | 38 | Logger.printDebug(() -> "Response: " + literalValue); 39 | 40 | if (literalValue != LITERAL_VALUE_AV01) 41 | return literalValue; 42 | 43 | final long currentTime = System.currentTimeMillis(); 44 | 45 | // Ignore the invoke within 20 seconds. 46 | if (currentTime - lastTimeResponse > 20000) { 47 | lastTimeResponse = currentTime; 48 | Utils.showToastShort(str("revanced_reject_av1_codec_toast")); 49 | } 50 | 51 | return LITERAL_VALUE_DOLBY_VISION; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/PlaybackSpeedMenuFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.shared.patches.components.Filter; 6 | import app.revanced.integrations.shared.patches.components.StringFilterGroup; 7 | import app.revanced.integrations.youtube.patches.video.CustomPlaybackSpeedPatch; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | /** 11 | * Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}. 12 | */ 13 | public final class PlaybackSpeedMenuFilter extends Filter { 14 | /** 15 | * Old litho based speed selection menu. 16 | */ 17 | public static volatile boolean isOldPlaybackSpeedMenuVisible; 18 | 19 | /** 20 | * 0.05x speed selection menu. 21 | */ 22 | public static volatile boolean isPlaybackRateSelectorMenuVisible; 23 | 24 | private final StringFilterGroup oldPlaybackMenuGroup; 25 | 26 | public PlaybackSpeedMenuFilter() { 27 | // 0.05x litho speed menu. 28 | final StringFilterGroup playbackRateSelectorGroup = new StringFilterGroup( 29 | Settings.ENABLE_CUSTOM_PLAYBACK_SPEED, 30 | "playback_rate_selector_menu_sheet.eml-js" 31 | ); 32 | 33 | // Old litho based speed menu. 34 | oldPlaybackMenuGroup = new StringFilterGroup( 35 | Settings.ENABLE_CUSTOM_PLAYBACK_SPEED, 36 | "playback_speed_sheet_content.eml-js"); 37 | 38 | addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup); 39 | } 40 | 41 | @Override 42 | public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, 43 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 44 | if (matchedGroup == oldPlaybackMenuGroup) { 45 | isOldPlaybackSpeedMenuVisible = true; 46 | } else { 47 | isPlaybackRateSelectorMenuVisible = true; 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/utils/DoubleBackToClosePatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.utils; 2 | 3 | import android.app.Activity; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | /** 10 | * @noinspection ALL 11 | */ 12 | public class DoubleBackToClosePatch { 13 | /** 14 | * Time between two back button presses 15 | */ 16 | private static final long PRESSED_TIMEOUT_MILLISECONDS = Settings.DOUBLE_BACK_TO_CLOSE_TIMEOUT.get(); 17 | 18 | /** 19 | * Last time back button was pressed 20 | */ 21 | private static long lastTimeBackPressed = 0; 22 | 23 | /** 24 | * State whether scroll position reaches the top 25 | */ 26 | private static boolean isScrollTop = false; 27 | 28 | /** 29 | * Detect event when back button is pressed 30 | * 31 | * @param activity is used when closing the app 32 | */ 33 | public static void closeActivityOnBackPressed(@NonNull Activity activity) { 34 | // Check scroll position reaches the top in home feed 35 | if (!isScrollTop) 36 | return; 37 | 38 | final long currentTime = System.currentTimeMillis(); 39 | 40 | // If the time between two back button presses does not reach PRESSED_TIMEOUT_MILLISECONDS, 41 | // set lastTimeBackPressed to the current time. 42 | if (currentTime - lastTimeBackPressed < PRESSED_TIMEOUT_MILLISECONDS || 43 | PRESSED_TIMEOUT_MILLISECONDS == 0) 44 | activity.finish(); 45 | else 46 | lastTimeBackPressed = currentTime; 47 | } 48 | 49 | /** 50 | * Detect event when ScrollView is created by RecyclerView 51 | * <p> 52 | * start of ScrollView 53 | */ 54 | public static void onStartScrollView() { 55 | isScrollTop = false; 56 | } 57 | 58 | /** 59 | * Detect event when the scroll position reaches the top by the back button 60 | * <p> 61 | * stop of ScrollView 62 | */ 63 | public static void onStopScrollView() { 64 | isScrollTop = true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/general/SettingsMenuPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.general; 2 | 3 | import androidx.preference.PreferenceScreen; 4 | 5 | import app.revanced.integrations.music.settings.Settings; 6 | import app.revanced.integrations.shared.patches.BaseSettingsMenuPatch; 7 | 8 | @SuppressWarnings("unused") 9 | public final class SettingsMenuPatch extends BaseSettingsMenuPatch { 10 | 11 | public static void hideSettingsMenu(PreferenceScreen mPreferenceScreen) { 12 | if (mPreferenceScreen == null) return; 13 | for (SettingsMenuComponent component : SettingsMenuComponent.values()) 14 | if (component.enabled) 15 | removePreference(mPreferenceScreen, component.key); 16 | } 17 | 18 | public static boolean hideParentToolsMenu(boolean original) { 19 | return !Settings.HIDE_SETTINGS_MENU_PARENT_TOOLS.get() && original; 20 | } 21 | 22 | private enum SettingsMenuComponent { 23 | GENERAL("settings_header_general", Settings.HIDE_SETTINGS_MENU_GENERAL.get()), 24 | PLAYBACK("settings_header_playback", Settings.HIDE_SETTINGS_MENU_PLAYBACK.get()), 25 | DATA_SAVING("settings_header_data_saving", Settings.HIDE_SETTINGS_MENU_DATA_SAVING.get()), 26 | DOWNLOADS_AND_STORAGE("settings_header_downloads_and_storage", Settings.HIDE_SETTINGS_MENU_DOWNLOADS_AND_STORAGE.get()), 27 | NOTIFICATIONS("settings_header_notifications", Settings.HIDE_SETTINGS_MENU_NOTIFICATIONS.get()), 28 | PRIVACY_AND_LOCATION("settings_header_privacy_and_location", Settings.HIDE_SETTINGS_MENU_PRIVACY_AND_LOCATION.get()), 29 | RECOMMENDATIONS("settings_header_recommendations", Settings.HIDE_SETTINGS_MENU_RECOMMENDATIONS.get()), 30 | PAID_MEMBERSHIPS("settings_header_paid_memberships", Settings.HIDE_SETTINGS_MENU_PAID_MEMBERSHIPS.get()), 31 | ABOUT("settings_header_about_youtube_music", Settings.HIDE_SETTINGS_MENU_ABOUT.get()); 32 | 33 | private final String key; 34 | private final boolean enabled; 35 | 36 | SettingsMenuComponent(String key, boolean enabled) { 37 | this.key = key; 38 | this.enabled = enabled; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/SanitizeUrlQueryPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches; 2 | 3 | import android.content.Intent; 4 | 5 | import app.revanced.integrations.shared.settings.BaseSettings; 6 | 7 | @SuppressWarnings("all") 8 | public final class SanitizeUrlQueryPatch { 9 | /** 10 | * This tracking parameter is mainly used. 11 | */ 12 | private static final String NEW_TRACKING_REGEX = ".si=.+"; 13 | /** 14 | * This tracking parameter is outdated. 15 | * Used when patching old versions or enabling spoof app version. 16 | */ 17 | private static final String OLD_TRACKING_REGEX = ".feature=.+"; 18 | private static final String URL_PROTOCOL = "http"; 19 | 20 | /** 21 | * Strip query parameters from a given URL string. 22 | * <p> 23 | * URL example containing tracking parameter: 24 | * https://youtu.be/ZWgr7qP6yhY?si=kKA_-9cygieuFY7R 25 | * https://youtu.be/ZWgr7qP6yhY?feature=shared 26 | * https://youtube.com/watch?v=ZWgr7qP6yhY&si=s_PZAxnJHKX1Mc8C 27 | * https://youtube.com/watch?v=ZWgr7qP6yhY&feature=shared 28 | * https://youtube.com/playlist?list=PLBsP89CPrMeO7uztAu6YxSB10cRMpjgiY&si=N0U8xncY2ZmQoSMp 29 | * https://youtube.com/playlist?list=PLBsP89CPrMeO7uztAu6YxSB10cRMpjgiY&feature=shared 30 | * <p> 31 | * Since we need to support support all these examples, 32 | * We cannot use [URL.getpath()] or [Uri.getQueryParameter()]. 33 | * 34 | * @param urlString URL string to strip query parameters from. 35 | * @return URL string without query parameters if possible, otherwise the original string. 36 | */ 37 | public static String stripQueryParameters(final String urlString) { 38 | if (!BaseSettings.SANITIZE_SHARING_LINKS.get()) 39 | return urlString; 40 | 41 | return urlString.replaceAll(NEW_TRACKING_REGEX, "").replaceAll(OLD_TRACKING_REGEX, ""); 42 | } 43 | 44 | public static void stripQueryParameters(final Intent intent, final String extraName, final String extraValue) { 45 | intent.putExtra(extraName, extraValue.startsWith(URL_PROTOCOL) 46 | ? stripQueryParameters(extraValue) 47 | : extraValue 48 | ); 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/preference/ReVancedPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.preference.Preference; 5 | import android.preference.PreferenceScreen; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import app.revanced.integrations.reddit.settings.preference.categories.AdsPreferenceCategory; 12 | import app.revanced.integrations.reddit.settings.preference.categories.LayoutPreferenceCategory; 13 | import app.revanced.integrations.reddit.settings.preference.categories.MiscellaneousPreferenceCategory; 14 | import app.revanced.integrations.shared.settings.Setting; 15 | import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment; 16 | 17 | /** 18 | * Preference fragment for ReVanced settings 19 | */ 20 | @SuppressWarnings("deprecation") 21 | public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { 22 | 23 | @Override 24 | protected void syncSettingWithPreference(@NonNull @NotNull Preference pref, 25 | @NonNull @NotNull Setting<?> setting, 26 | boolean applySettingToPreference) { 27 | super.syncSettingWithPreference(pref, setting, applySettingToPreference); 28 | } 29 | 30 | @Override 31 | protected void initialize() { 32 | final Context context = getContext(); 33 | 34 | // Currently no resources can be compiled for Reddit (fails with aapt error). 35 | // So all Reddit Strings are hard coded in integrations. 36 | restartDialogTitle = "Restart required"; 37 | restartDialogMessage = "Restart the app for this change to take effect."; 38 | 39 | PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); 40 | setPreferenceScreen(preferenceScreen); 41 | 42 | // Custom categories reference app specific Settings class. 43 | new AdsPreferenceCategory(context, preferenceScreen); 44 | new LayoutPreferenceCategory(context, preferenceScreen); 45 | new MiscellaneousPreferenceCategory(context, preferenceScreen); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/general/YouTubeMusicActionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.general; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import app.revanced.integrations.shared.settings.Setting; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | import app.revanced.integrations.youtube.utils.ExtendedUtils; 10 | import app.revanced.integrations.youtube.utils.VideoUtils; 11 | 12 | @SuppressWarnings("unused") 13 | public final class YouTubeMusicActionsPatch extends VideoUtils { 14 | 15 | private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music"; 16 | 17 | private static final boolean isOverrideYouTubeMusicEnabled = 18 | Settings.OVERRIDE_YOUTUBE_MUSIC_BUTTON.get(); 19 | 20 | private static final boolean overrideYouTubeMusicEnabled = 21 | isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled(); 22 | 23 | public static String overridePackageName(@NonNull String packageName) { 24 | if (!overrideYouTubeMusicEnabled) { 25 | return packageName; 26 | } 27 | if (!StringUtils.equals(PACKAGE_NAME_YOUTUBE_MUSIC, packageName)) { 28 | return packageName; 29 | } 30 | final String thirdPartyPackageName = Settings.THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME.get(); 31 | if (!ExtendedUtils.isPackageEnabled(thirdPartyPackageName)) { 32 | return packageName; 33 | } 34 | return thirdPartyPackageName; 35 | } 36 | 37 | private static boolean isYouTubeMusicEnabled() { 38 | return ExtendedUtils.isPackageEnabled(PACKAGE_NAME_YOUTUBE_MUSIC); 39 | } 40 | 41 | public static final class HookYouTubeMusicAvailability implements Setting.Availability { 42 | @Override 43 | public boolean isAvailable() { 44 | return isYouTubeMusicEnabled(); 45 | } 46 | } 47 | 48 | public static final class HookYouTubeMusicPackageNameAvailability implements Setting.Availability { 49 | @Override 50 | public boolean isAvailable() { 51 | return isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled(); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/spans/FilterGroupList.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.spans; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.Spliterator; 10 | import java.util.function.Consumer; 11 | 12 | import app.revanced.integrations.shared.utils.TrieSearch; 13 | 14 | public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> { 15 | 16 | private final List<T> filterGroups = new ArrayList<>(); 17 | private final TrieSearch<V> search = createSearchGraph(); 18 | 19 | @SafeVarargs 20 | protected final void addAll(final T... groups) { 21 | filterGroups.addAll(Arrays.asList(groups)); 22 | 23 | for (T group : groups) { 24 | if (!group.includeInSearch()) { 25 | continue; 26 | } 27 | for (V pattern : group.filters) { 28 | search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { 29 | if (group.isEnabled()) { 30 | FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; 31 | result.setValues(group.setting, matchedStartIndex); 32 | return true; 33 | } 34 | return false; 35 | }); 36 | } 37 | } 38 | } 39 | 40 | @NonNull 41 | @Override 42 | public Iterator<T> iterator() { 43 | return filterGroups.iterator(); 44 | } 45 | 46 | @Override 47 | public void forEach(@NonNull Consumer<? super T> action) { 48 | filterGroups.forEach(action); 49 | } 50 | 51 | @NonNull 52 | @Override 53 | public Spliterator<T> spliterator() { 54 | return filterGroups.spliterator(); 55 | } 56 | 57 | protected FilterGroup.FilterGroupResult check(V stack) { 58 | FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); 59 | search.matches(stack, result); 60 | return result; 61 | 62 | } 63 | 64 | protected abstract TrieSearch<V> createSearchGraph(); 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/navigation/NavigationPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.navigation; 2 | 3 | import static app.revanced.integrations.shared.utils.Utils.hideViewUnderCondition; 4 | 5 | import android.graphics.Color; 6 | import android.view.View; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import app.revanced.integrations.music.settings.Settings; 12 | import app.revanced.integrations.shared.utils.ResourceUtils; 13 | 14 | @SuppressWarnings("unused") 15 | public class NavigationPatch { 16 | private static final int colorGrey12 = 17 | ResourceUtils.getColor("revanced_color_grey_12"); 18 | public static Enum<?> lastPivotTab; 19 | 20 | public static int enableBlackNavigationBar() { 21 | return Settings.ENABLE_BLACK_NAVIGATION_BAR.get() 22 | ? Color.BLACK 23 | : colorGrey12; 24 | } 25 | 26 | public static void hideNavigationLabel(TextView textview) { 27 | hideViewUnderCondition(Settings.HIDE_NAVIGATION_LABEL.get(), textview); 28 | } 29 | 30 | public static void hideNavigationButton(@NonNull View view) { 31 | if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) { 32 | hideViewUnderCondition(true, (View) view.getParent()); 33 | return; 34 | } 35 | 36 | for (NavigationButton button : NavigationButton.values()) 37 | if (lastPivotTab.name().equals(button.name)) 38 | hideViewUnderCondition(button.enabled, view); 39 | } 40 | 41 | private enum NavigationButton { 42 | HOME("TAB_HOME", Settings.HIDE_NAVIGATION_HOME_BUTTON.get()), 43 | SAMPLES("TAB_SAMPLES", Settings.HIDE_NAVIGATION_SAMPLES_BUTTON.get()), 44 | EXPLORE("TAB_EXPLORE", Settings.HIDE_NAVIGATION_EXPLORE_BUTTON.get()), 45 | LIBRARY("LIBRARY_MUSIC", Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get()), 46 | UPGRADE("TAB_MUSIC_PREMIUM", Settings.HIDE_NAVIGATION_UPGRADE_BUTTON.get()); 47 | 48 | private final boolean enabled; 49 | private final String name; 50 | 51 | NavigationButton(String name, boolean enabled) { 52 | this.enabled = enabled; 53 | this.name = name; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/settings/preference/ResettableEditTextPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.settings.preference; 2 | 3 | import static app.revanced.integrations.music.utils.ExtendedUtils.getDialogBuilder; 4 | import static app.revanced.integrations.music.utils.ExtendedUtils.getLayoutParams; 5 | import static app.revanced.integrations.shared.utils.StringRef.str; 6 | 7 | import android.app.Activity; 8 | import android.widget.EditText; 9 | import android.widget.FrameLayout; 10 | 11 | import androidx.annotation.NonNull; 12 | 13 | import com.google.android.material.textfield.TextInputLayout; 14 | 15 | import app.revanced.integrations.shared.settings.Setting; 16 | import app.revanced.integrations.shared.utils.Logger; 17 | 18 | public class ResettableEditTextPreference { 19 | 20 | public static void showDialog(Activity mActivity, @NonNull Setting<String> setting) { 21 | try { 22 | final EditText textView = new EditText(mActivity); 23 | textView.setText(setting.get()); 24 | 25 | TextInputLayout textInputLayout = new TextInputLayout(mActivity); 26 | textInputLayout.setLayoutParams(getLayoutParams()); 27 | textInputLayout.addView(textView); 28 | 29 | FrameLayout container = new FrameLayout(mActivity); 30 | container.addView(textInputLayout); 31 | 32 | getDialogBuilder(mActivity) 33 | .setTitle(str(setting.key + "_title")) 34 | .setView(container) 35 | .setNegativeButton(android.R.string.cancel, null) 36 | .setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> { 37 | setting.resetToDefault(); 38 | ReVancedPreferenceFragment.showRebootDialog(); 39 | }) 40 | .setPositiveButton(android.R.string.ok, (dialog, which) -> { 41 | setting.save(textView.getText().toString().trim()); 42 | ReVancedPreferenceFragment.showRebootDialog(); 43 | }) 44 | .show(); 45 | } catch (Exception ex) { 46 | Logger.printException(() -> "showDialog failure", ex); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/requests/Route.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.requests; 2 | 3 | public class Route { 4 | private final String route; 5 | private final Route.Method method; 6 | private final int paramCount; 7 | 8 | public Route(Route.Method method, String route) { 9 | this.method = method; 10 | this.route = route; 11 | this.paramCount = countMatches(route, '{'); 12 | 13 | if (paramCount != countMatches(route, '}')) 14 | throw new IllegalArgumentException("Not enough parameters"); 15 | } 16 | 17 | public Route.Method getMethod() { 18 | return method; 19 | } 20 | 21 | public Route.CompiledRoute compile(String... params) { 22 | if (params.length != paramCount) 23 | throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " + 24 | "Expected: " + paramCount + ", provided: " + params.length); 25 | 26 | StringBuilder compiledRoute = new StringBuilder(route); 27 | for (int i = 0; i < paramCount; i++) { 28 | int paramStart = compiledRoute.indexOf("{"); 29 | int paramEnd = compiledRoute.indexOf("}"); 30 | compiledRoute.replace(paramStart, paramEnd + 1, params[i]); 31 | } 32 | return new Route.CompiledRoute(this, compiledRoute.toString()); 33 | } 34 | 35 | private int countMatches(CharSequence seq, char c) { 36 | int count = 0; 37 | for (int i = 0; i < seq.length(); i++) { 38 | if (seq.charAt(i) == c) 39 | count++; 40 | } 41 | return count; 42 | } 43 | 44 | public enum Method { 45 | GET, 46 | POST 47 | } 48 | 49 | public static class CompiledRoute { 50 | private final Route baseRoute; 51 | private final String compiledRoute; 52 | 53 | private CompiledRoute(Route baseRoute, String compiledRoute) { 54 | this.baseRoute = baseRoute; 55 | this.compiledRoute = compiledRoute; 56 | } 57 | 58 | public String getCompiledRoute() { 59 | return compiledRoute; 60 | } 61 | 62 | public Route.Method getMethod() { 63 | return baseRoute.method; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spans/SearchLinksFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spans; 2 | 3 | import android.text.SpannableString; 4 | 5 | import app.revanced.integrations.shared.patches.spans.Filter; 6 | import app.revanced.integrations.shared.patches.spans.SpanType; 7 | import app.revanced.integrations.shared.patches.spans.StringFilterGroup; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | @SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"}) 11 | public final class SearchLinksFilter extends Filter { 12 | /** 13 | * Located in front of the search icon. 14 | */ 15 | private final String WORD_JOINER_CHARACTER = "\u2060"; 16 | 17 | public SearchLinksFilter() { 18 | addCallbacks( 19 | new StringFilterGroup( 20 | Settings.HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS, 21 | "|comment." 22 | ) 23 | ); 24 | } 25 | 26 | /** 27 | * @return Whether the word contains a search icon or not. 28 | */ 29 | private boolean isSearchLinks(SpannableString original, int end) { 30 | String originalString = original.toString(); 31 | int wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER); 32 | // There may be more than one highlight keyword in the comment. 33 | // Check the index of all highlight keywords. 34 | while (wordJoinerIndex != -1) { 35 | if (end - wordJoinerIndex == 2) return true; 36 | wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER, wordJoinerIndex + 1); 37 | } 38 | return false; 39 | } 40 | 41 | @Override 42 | public boolean skip(String conversionContext, SpannableString spannableString, Object span, 43 | int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) { 44 | if (isWord && isSearchLinks(spannableString, end)) { 45 | if (spanType == SpanType.IMAGE) { 46 | hideSpan(spannableString, start, end, flags); 47 | } 48 | return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup); 49 | } 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/video/ReloadVideoPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.video; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.str; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import app.revanced.integrations.shared.utils.Utils; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | import app.revanced.integrations.youtube.shared.PlayerType; 10 | import app.revanced.integrations.youtube.shared.VideoInformation; 11 | 12 | @SuppressWarnings("unused") 13 | public class ReloadVideoPatch { 14 | private static final long RELOAD_VIDEO_TIME_MILLISECONDS = 15000L; 15 | 16 | @NonNull 17 | public static String videoId = ""; 18 | 19 | /** 20 | * Injection point. 21 | */ 22 | public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, 23 | @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, 24 | final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { 25 | if (!Settings.SKIP_PRELOADED_BUFFER.get()) 26 | return; 27 | if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) 28 | return; 29 | if (videoId.equals(newlyLoadedVideoId)) 30 | return; 31 | videoId = newlyLoadedVideoId; 32 | 33 | if (newlyLoadedVideoLength < RELOAD_VIDEO_TIME_MILLISECONDS || newlyLoadedLiveStreamValue) 34 | return; 35 | 36 | final long seekTime = Math.max(RELOAD_VIDEO_TIME_MILLISECONDS, (long) (newlyLoadedVideoLength * 0.5)); 37 | 38 | Utils.runOnMainThreadDelayed(() -> reloadVideo(seekTime), 250); 39 | } 40 | 41 | private static void reloadVideo(final long videoLength) { 42 | final long lastVideoTime = VideoInformation.getVideoTime(); 43 | final float playbackSpeed = VideoInformation.getPlaybackSpeed(); 44 | final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 300); 45 | VideoInformation.overrideVideoTime(videoLength); 46 | VideoInformation.overrideVideoTime(lastVideoTime + speedAdjustedTimeThreshold); 47 | 48 | if (!Settings.SKIP_PRELOADED_BUFFER_TOAST.get()) 49 | return; 50 | 51 | Utils.showToastShort(str("revanced_skipped_preloaded_buffer")); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/components/FilterGroupList.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.components; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.RequiresApi; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | import java.util.Spliterator; 11 | import java.util.function.Consumer; 12 | 13 | import app.revanced.integrations.shared.utils.TrieSearch; 14 | 15 | @SuppressWarnings("unused") 16 | public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> { 17 | 18 | private final List<T> filterGroups = new ArrayList<>(); 19 | private final TrieSearch<V> search = createSearchGraph(); 20 | 21 | @SafeVarargs 22 | public final void addAll(final T... groups) { 23 | filterGroups.addAll(Arrays.asList(groups)); 24 | 25 | for (T group : groups) { 26 | if (!group.includeInSearch()) { 27 | continue; 28 | } 29 | for (V pattern : group.filters) { 30 | search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { 31 | if (group.isEnabled()) { 32 | FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; 33 | result.setValues(group.setting, matchedStartIndex, matchedLength); 34 | return true; 35 | } 36 | return false; 37 | }); 38 | } 39 | } 40 | } 41 | 42 | @NonNull 43 | @Override 44 | public Iterator<T> iterator() { 45 | return filterGroups.iterator(); 46 | } 47 | 48 | @RequiresApi(24) 49 | @Override 50 | public void forEach(@NonNull Consumer<? super T> action) { 51 | filterGroups.forEach(action); 52 | } 53 | 54 | @RequiresApi(24) 55 | @NonNull 56 | @Override 57 | public Spliterator<T> spliterator() { 58 | return filterGroups.spliterator(); 59 | } 60 | 61 | public FilterGroup.FilterGroupResult check(V stack) { 62 | FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); 63 | search.matches(stack, result); 64 | return result; 65 | 66 | } 67 | 68 | protected abstract TrieSearch<V> createSearchGraph(); 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/DeviceHardwareSupport.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.misc.client; 2 | 3 | import static app.revanced.integrations.shared.utils.Utils.isSDKAbove; 4 | 5 | import android.media.MediaCodecInfo; 6 | import android.media.MediaCodecList; 7 | 8 | import app.revanced.integrations.shared.utils.Logger; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | 11 | public class DeviceHardwareSupport { 12 | private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; 13 | private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; 14 | 15 | static { 16 | boolean vp9found = false; 17 | boolean av1found = false; 18 | MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); 19 | final boolean deviceIsAndroidTenOrLater = isSDKAbove(29); 20 | 21 | for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { 22 | final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater 23 | ? codecInfo.isHardwareAccelerated() 24 | : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. 25 | if (isHardwareAccelerated && !codecInfo.isEncoder()) { 26 | for (String type : codecInfo.getSupportedTypes()) { 27 | if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { 28 | vp9found = true; 29 | } else if (type.equalsIgnoreCase("video/av01")) { 30 | av1found = true; 31 | } 32 | } 33 | } 34 | } 35 | 36 | DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; 37 | DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; 38 | 39 | Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 40 | ? "Device supports AV1 hardware decoding\n" 41 | : "Device does not support AV1 hardware decoding\n" 42 | + (DEVICE_HAS_HARDWARE_DECODING_VP9 43 | ? "Device supports VP9 hardware decoding" 44 | : "Device does not support VP9 hardware decoding")); 45 | } 46 | 47 | public static boolean allowVP9() { 48 | return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get(); 49 | } 50 | 51 | public static boolean allowAV1() { 52 | return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/utils/BaseThemeUtils.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.utils; 2 | 3 | import static app.revanced.integrations.shared.utils.ResourceUtils.getColor; 4 | import static app.revanced.integrations.shared.utils.ResourceUtils.getColorIdentifier; 5 | 6 | import android.graphics.Color; 7 | 8 | @SuppressWarnings("unused") 9 | public class BaseThemeUtils { 10 | private static int themeValue = 1; 11 | 12 | /** 13 | * Injection point. 14 | */ 15 | public static void setTheme(Enum<?> value) { 16 | final int newOrdinalValue = value.ordinal(); 17 | if (themeValue != newOrdinalValue) { 18 | themeValue = newOrdinalValue; 19 | Logger.printDebug(() -> "Theme value: " + newOrdinalValue); 20 | } 21 | } 22 | 23 | public static boolean isDarkTheme() { 24 | return themeValue == 1; 25 | } 26 | 27 | public static String getColorHexString(int color) { 28 | return String.format("#%06X", (0xFFFFFF & color)); 29 | } 30 | 31 | /** 32 | * Subclasses can override this and provide a themed color. 33 | */ 34 | public static int getLightColor() { 35 | return Color.WHITE; 36 | } 37 | 38 | /** 39 | * Subclasses can override this and provide a themed color. 40 | */ 41 | public static int getDarkColor() { 42 | return Color.BLACK; 43 | } 44 | 45 | public static String getBackgroundColorHexString() { 46 | return getColorHexString(getBackgroundColor()); 47 | } 48 | 49 | public static String getForegroundColorHexString() { 50 | return getColorHexString(getForegroundColor()); 51 | } 52 | 53 | public static int getBackgroundColor() { 54 | final String colorName = isDarkTheme() ? "yt_black1" : "yt_white1"; 55 | final int colorIdentifier = getColorIdentifier(colorName); 56 | if (colorIdentifier != 0) { 57 | return getColor(colorName); 58 | } else { 59 | return isDarkTheme() ? getDarkColor() : getLightColor(); 60 | } 61 | } 62 | 63 | public static int getForegroundColor() { 64 | final String colorName = isDarkTheme() ? "yt_white1" : "yt_black1"; 65 | final int colorIdentifier = getColorIdentifier(colorName); 66 | if (colorIdentifier != 0) { 67 | return getColor(colorName); 68 | } else { 69 | return isDarkTheme() ? getLightColor() : getDarkColor(); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/ActivityHook.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.view.View; 7 | import android.widget.FrameLayout; 8 | import android.widget.LinearLayout; 9 | 10 | import app.revanced.integrations.reddit.settings.preference.ReVancedPreferenceFragment; 11 | import app.revanced.integrations.shared.utils.ResourceUtils; 12 | 13 | @SuppressWarnings("all") 14 | public class ActivityHook { 15 | 16 | public static int getIcon() { 17 | return ResourceUtils.getDrawableIdentifier("icon_ai"); 18 | } 19 | 20 | public static boolean hook(Activity activity) { 21 | Intent intent = activity.getIntent(); 22 | if ("RVX".equals(intent.getStringExtra("com.reddit.extra.initial_url"))) { 23 | initialize(activity); 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | public static void initialize(Activity activity) { 31 | SettingsStatus.load(); 32 | 33 | final int fragmentId = View.generateViewId(); 34 | final FrameLayout fragment = new FrameLayout(activity); 35 | fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1)); 36 | fragment.setId(fragmentId); 37 | 38 | final LinearLayout linearLayout = new LinearLayout(activity); 39 | linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)); 40 | linearLayout.setOrientation(LinearLayout.VERTICAL); 41 | linearLayout.setFitsSystemWindows(true); 42 | linearLayout.setTransitionGroup(true); 43 | linearLayout.addView(fragment); 44 | linearLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); 45 | activity.setContentView(linearLayout); 46 | 47 | activity.getFragmentManager() 48 | .beginTransaction() 49 | .replace(fragmentId, new ReVancedPreferenceFragment()) 50 | .commit(); 51 | } 52 | 53 | public static boolean isAcknowledgment(Enum<?> e) { 54 | return e != null && "ACKNOWLEDGMENTS".equals(e.name()); 55 | } 56 | 57 | public static Intent initializeByIntent(Context context) { 58 | Intent intent = new Intent(); 59 | intent.setClassName(context, "com.reddit.webembed.browser.WebBrowserActivity"); 60 | intent.putExtra("com.reddit.extra.initial_url", "RVX"); 61 | return intent; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/music/patches/account/AccountPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.music.patches.account; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.str; 4 | import static app.revanced.integrations.shared.utils.Utils.isSDKAbove; 5 | 6 | import android.view.View; 7 | import android.widget.TextView; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.Objects; 13 | 14 | import app.revanced.integrations.music.settings.Settings; 15 | 16 | @SuppressWarnings("unused") 17 | public class AccountPatch { 18 | 19 | private static String[] accountMenuBlockList; 20 | 21 | static { 22 | accountMenuBlockList = Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS.get().split("\\n"); 23 | // Some settings should not be hidden. 24 | if (isSDKAbove(24)) { 25 | accountMenuBlockList = Arrays.stream(accountMenuBlockList) 26 | .filter(item -> !Objects.equals(item, str("settings"))) 27 | .toArray(String[]::new); 28 | } else { 29 | List<String> tmp = new ArrayList<>(Arrays.asList(accountMenuBlockList)); 30 | tmp.remove(str("settings")); // "Settings" should appear only once in the account menu 31 | accountMenuBlockList = tmp.toArray(new String[0]); 32 | } 33 | } 34 | 35 | public static void hideAccountMenu(CharSequence charSequence, View view) { 36 | if (!Settings.HIDE_ACCOUNT_MENU.get()) 37 | return; 38 | 39 | if (charSequence == null) { 40 | if (Settings.HIDE_ACCOUNT_MENU_EMPTY_COMPONENT.get()) 41 | view.setVisibility(View.GONE); 42 | 43 | return; 44 | } 45 | 46 | for (String filter : accountMenuBlockList) { 47 | if (!filter.isEmpty() && charSequence.toString().equals(filter)) 48 | view.setVisibility(View.GONE); 49 | } 50 | } 51 | 52 | public static boolean hideHandle(boolean original) { 53 | return Settings.HIDE_HANDLE.get() || original; 54 | } 55 | 56 | public static void hideHandle(TextView textView, int visibility) { 57 | final int finalVisibility = Settings.HIDE_HANDLE.get() 58 | ? View.GONE 59 | : visibility; 60 | textView.setVisibility(finalVisibility); 61 | } 62 | 63 | public static int hideTermsContainer() { 64 | return Settings.HIDE_TERMS_CONTAINER.get() ? View.GONE : View.VISIBLE; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/preference/WebViewDialog.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings.preference; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import android.view.Window; 10 | import android.webkit.WebView; 11 | import android.webkit.WebViewClient; 12 | 13 | import androidx.annotation.NonNull; 14 | 15 | import app.revanced.integrations.shared.utils.Logger; 16 | import app.revanced.integrations.shared.utils.Utils; 17 | 18 | /** 19 | * Displays html content as a dialog. Any links a user taps on are opened in an external browser. 20 | */ 21 | @SuppressWarnings("deprecation") 22 | public class WebViewDialog extends Dialog { 23 | 24 | private final String htmlContent; 25 | 26 | public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) { 27 | super(context); 28 | this.htmlContent = htmlContent; 29 | } 30 | 31 | // JS required to hide any broken images. No remote javascript is ever loaded. 32 | @SuppressLint("SetJavaScriptEnabled") 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | requestWindowFeature(Window.FEATURE_NO_TITLE); 37 | 38 | WebView webView = new WebView(getContext()); 39 | webView.getSettings().setJavaScriptEnabled(true); 40 | webView.setWebViewClient(new OpenLinksExternallyWebClient()); 41 | webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); 42 | 43 | setContentView(webView); 44 | } 45 | 46 | private class OpenLinksExternallyWebClient extends WebViewClient { 47 | @Override 48 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 49 | try { 50 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 51 | getContext().startActivity(intent); 52 | } catch (Exception ex) { 53 | Logger.printException(() -> "Open link failure", ex); 54 | } 55 | // Dismiss the about dialog using a delay, 56 | // otherwise without a delay the UI looks hectic with the dialog dismissing 57 | // to show the settings while simultaneously a web browser is opening. 58 | Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500); 59 | return true; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/ScreenBrightnessController.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.controller 2 | 3 | import android.view.WindowManager 4 | import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity 5 | import app.revanced.integrations.youtube.swipecontrols.misc.clamp 6 | 7 | /** 8 | * controller to adjust the screen brightness level 9 | * 10 | * @param host the host activity of which the brightness is adjusted, the main controller instance 11 | */ 12 | class ScreenBrightnessController( 13 | val host: SwipeControlsHostActivity, 14 | ) { 15 | 16 | /** 17 | * the current screen brightness in percent, ranging from 0.0 to 100.0 18 | */ 19 | var screenBrightness: Double 20 | get() = rawScreenBrightness * 100.0 21 | set(value) { 22 | rawScreenBrightness = (value.toFloat() / 100f).clamp(0f, 1f) 23 | } 24 | 25 | /** 26 | * restore the screen brightness to the default device brightness 27 | */ 28 | fun restoreDefaultBrightness() { 29 | rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE 30 | } 31 | 32 | // Flag that indicates whether the brightness has been restored 33 | private var isBrightnessRestored = false 34 | 35 | /** 36 | * save the current screen brightness into settings, to be brought back using [restore] 37 | */ 38 | fun save() { 39 | if (isBrightnessRestored) { 40 | // Saves the current screen brightness value into settings 41 | host.config.savedScreenBrightnessValue = rawScreenBrightness 42 | // Reset the flag 43 | isBrightnessRestored = false 44 | } 45 | } 46 | 47 | /** 48 | * restore the screen brightness from settings saved using [save] 49 | */ 50 | fun restore() { 51 | // Restores the screen brightness value from the saved settings 52 | rawScreenBrightness = host.config.savedScreenBrightnessValue 53 | // Mark that brightness has been restored 54 | isBrightnessRestored = true 55 | } 56 | 57 | /** 58 | * wrapper for the raw screen brightness in [WindowManager.LayoutParams.screenBrightness] 59 | */ 60 | private var rawScreenBrightness: Float 61 | get() = host.window.attributes.screenBrightness 62 | private set(value) { 63 | val attr = host.window.attributes 64 | attr.screenBrightness = value 65 | host.window.attributes = attr 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | alias(libs.plugins.android.application) 5 | alias(libs.plugins.kotlin) 6 | publishing 7 | } 8 | 9 | android { 10 | namespace = "app.revanced.integrations" 11 | compileSdk = 34 12 | 13 | applicationVariants.all { 14 | outputs.all { 15 | this as com.android.build.gradle.internal.api.ApkVariantOutputImpl 16 | 17 | outputFileName = "${rootProject.name}-$versionName.apk" 18 | } 19 | } 20 | 21 | defaultConfig { 22 | applicationId = "app.revanced.integrations" 23 | minSdk = 24 24 | targetSdk = 34 25 | multiDexEnabled = false 26 | versionName = version as String 27 | 28 | buildConfigField("String", "VERSION_NAME", "\"${versionName}\"") 29 | } 30 | 31 | buildFeatures { 32 | buildConfig = true 33 | } 34 | 35 | buildTypes { 36 | release { 37 | isMinifyEnabled = true 38 | proguardFiles( 39 | getDefaultProguardFile("proguard-android-optimize.txt"), 40 | "proguard-rules.pro", 41 | ) 42 | } 43 | } 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_17 47 | targetCompatibility = JavaVersion.VERSION_17 48 | } 49 | 50 | kotlinOptions { 51 | jvmTarget = JavaVersion.VERSION_17.toString() 52 | } 53 | } 54 | 55 | dependencies { 56 | compileOnly(libs.annotation) 57 | compileOnly(libs.preference) 58 | implementation(libs.lang3) 59 | 60 | compileOnly(project(":stub")) 61 | } 62 | 63 | tasks { 64 | // Because the signing plugin doesn't support signing APKs, do it manually. 65 | register("sign") { 66 | group = "signing" 67 | 68 | dependsOn(build) 69 | 70 | doLast { 71 | val outputDirectory = layout.buildDirectory.dir("outputs/apk/release").get().asFile 72 | val integrationsApk = outputDirectory.resolve("${rootProject.name}-$version.apk") 73 | 74 | org.gradle.security.internal.gnupg.GnupgSignatoryFactory().createSignatory(project).sign( 75 | integrationsApk.inputStream(), 76 | outputDirectory.resolve("${integrationsApk.name}.asc").outputStream(), 77 | ) 78 | } 79 | } 80 | 81 | // Needed by gradle-semantic-release-plugin. 82 | // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 83 | publish { 84 | dependsOn(build) 85 | dependsOn("sign") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/FullscreenAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches; 2 | 3 | import static app.revanced.integrations.shared.utils.Utils.hideViewBy0dpUnderCondition; 4 | 5 | import android.view.View; 6 | 7 | import app.revanced.integrations.shared.patches.components.ByteArrayFilterGroup; 8 | import app.revanced.integrations.shared.settings.BaseSettings; 9 | import app.revanced.integrations.shared.utils.Logger; 10 | 11 | @SuppressWarnings("unused") 12 | public class FullscreenAdsPatch { 13 | private static final boolean hideFullscreenAdsEnabled = BaseSettings.HIDE_FULLSCREEN_ADS.get(); 14 | private static final ByteArrayFilterGroup exception = 15 | new ByteArrayFilterGroup( 16 | null, 17 | "post_image_lightbox.eml" // Community post image in fullscreen 18 | ); 19 | 20 | public static boolean disableFullscreenAds(final byte[] bytes, int type) { 21 | if (!hideFullscreenAdsEnabled) { 22 | return false; 23 | } 24 | 25 | final DialogType dialogType = DialogType.getDialogType(type); 26 | final String dialogName = dialogType.name(); 27 | 28 | // The dialog type of a fullscreen dialog is always {@code DialogType.FULLSCREEN} 29 | if (dialogType != DialogType.FULLSCREEN) { 30 | Logger.printDebug(() -> "Ignoring dialogType " + dialogName); 31 | return false; 32 | } 33 | 34 | // Image in community post in fullscreen is not filtered 35 | final boolean isException = bytes != null && 36 | exception.check(bytes).isFiltered(); 37 | 38 | if (isException) { 39 | Logger.printDebug(() -> "Ignoring exception"); 40 | } else { 41 | Logger.printDebug(() -> "Blocked fullscreen ads"); 42 | } 43 | 44 | return !isException; 45 | } 46 | 47 | public static void hideFullscreenAds(View view) { 48 | hideViewBy0dpUnderCondition( 49 | hideFullscreenAdsEnabled, 50 | view 51 | ); 52 | } 53 | 54 | private enum DialogType { 55 | NULL(0), 56 | ALERT(1), 57 | FULLSCREEN(2), 58 | LAYOUT_FULLSCREEN(3); 59 | 60 | private final int type; 61 | 62 | DialogType(int type) { 63 | this.type = type; 64 | } 65 | 66 | private static DialogType getDialogType(int type) { 67 | for (DialogType val : values()) 68 | if (type == val.type) return val; 69 | 70 | return DialogType.NULL; 71 | } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/patches/spans/FilterGroup.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.patches.spans; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import app.revanced.integrations.shared.settings.BooleanSetting; 6 | 7 | public abstract class FilterGroup<T> { 8 | final static class FilterGroupResult { 9 | private BooleanSetting setting; 10 | private int matchedIndex; 11 | // In the future it might be useful to include which pattern matched, 12 | // but for now that is not needed. 13 | 14 | FilterGroupResult() { 15 | this(null, -1); 16 | } 17 | 18 | FilterGroupResult(BooleanSetting setting, int matchedIndex) { 19 | setValues(setting, matchedIndex); 20 | } 21 | 22 | public void setValues(BooleanSetting setting, int matchedIndex) { 23 | this.setting = setting; 24 | this.matchedIndex = matchedIndex; 25 | } 26 | 27 | /** 28 | * A null value if the group has no setting, 29 | * or if no match is returned from {@link FilterGroupList#check(Object)}. 30 | */ 31 | public BooleanSetting getSetting() { 32 | return setting; 33 | } 34 | 35 | public boolean isFiltered() { 36 | return matchedIndex >= 0; 37 | } 38 | } 39 | 40 | protected final BooleanSetting setting; 41 | protected final T[] filters; 42 | 43 | /** 44 | * Initialize a new filter group. 45 | * 46 | * @param setting The associated setting. 47 | * @param filters The filters. 48 | */ 49 | @SafeVarargs 50 | public FilterGroup(final BooleanSetting setting, final T... filters) { 51 | this.setting = setting; 52 | this.filters = filters; 53 | if (filters.length == 0) { 54 | throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); 55 | } 56 | } 57 | 58 | public boolean isEnabled() { 59 | return setting == null || setting.get(); 60 | } 61 | 62 | /** 63 | * @return If {@link FilterGroupList} should include this group when searching. 64 | * By default, all filters are included except non enabled settings that require reboot. 65 | */ 66 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 67 | public boolean includeInSearch() { 68 | return isEnabled() || !setting.rebootApp; 69 | } 70 | 71 | @NonNull 72 | @Override 73 | public String toString() { 74 | return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); 75 | } 76 | 77 | public abstract FilterGroupResult check(final T stack); 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/general/LayoutSwitchPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.general; 2 | 3 | import static java.lang.Boolean.FALSE; 4 | import static java.lang.Boolean.TRUE; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import org.apache.commons.lang3.BooleanUtils; 9 | 10 | import java.util.Objects; 11 | 12 | import app.revanced.integrations.shared.utils.PackageUtils; 13 | import app.revanced.integrations.youtube.settings.Settings; 14 | 15 | @SuppressWarnings("unused") 16 | public final class LayoutSwitchPatch { 17 | 18 | public enum FormFactor { 19 | /** 20 | * Unmodified type, and same as un-patched. 21 | */ 22 | ORIGINAL(null, null, null), 23 | SMALL_FORM_FACTOR(1, null, TRUE), 24 | SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE), 25 | LARGE_FORM_FACTOR(2, null, FALSE), 26 | LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE); 27 | 28 | @Nullable 29 | final Integer formFactorType; 30 | 31 | @Nullable 32 | final Integer widthDp; 33 | 34 | @Nullable 35 | final Boolean setMinimumDp; 36 | 37 | FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) { 38 | this.formFactorType = formFactorType; 39 | this.widthDp = widthDp; 40 | this.setMinimumDp = setMinimumDp; 41 | } 42 | 43 | private boolean setMinimumDp() { 44 | return BooleanUtils.isTrue(setMinimumDp); 45 | } 46 | } 47 | 48 | private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get(); 49 | 50 | public static int getFormFactor(int original) { 51 | Integer formFactorType = FORM_FACTOR.formFactorType; 52 | return formFactorType == null 53 | ? original 54 | : formFactorType; 55 | } 56 | 57 | public static int getWidthDp(int original) { 58 | Integer widthDp = FORM_FACTOR.widthDp; 59 | if (widthDp == null) { 60 | return original; 61 | } 62 | final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp(); 63 | if (smallestScreenWidthDp == 0) { 64 | return original; 65 | } 66 | return FORM_FACTOR.setMinimumDp() 67 | ? Math.min(smallestScreenWidthDp, widthDp) 68 | : Math.max(smallestScreenWidthDp, widthDp); 69 | } 70 | 71 | public static boolean phoneLayoutEnabled() { 72 | return Objects.equals(FORM_FACTOR.formFactorType, 1); 73 | } 74 | 75 | public static boolean tabletLayoutEnabled() { 76 | return Objects.equals(FORM_FACTOR.formFactorType, 2); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/overlaybutton/SpeedDialog.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.overlaybutton; 2 | 3 | import static app.revanced.integrations.shared.utils.StringRef.str; 4 | import static app.revanced.integrations.shared.utils.Utils.showToastShort; 5 | 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.Nullable; 10 | 11 | import app.revanced.integrations.shared.utils.Logger; 12 | import app.revanced.integrations.youtube.settings.Settings; 13 | import app.revanced.integrations.youtube.shared.VideoInformation; 14 | import app.revanced.integrations.youtube.utils.VideoUtils; 15 | 16 | @SuppressWarnings("unused") 17 | public class SpeedDialog extends BottomControlButton { 18 | @Nullable 19 | private static SpeedDialog instance; 20 | 21 | public SpeedDialog(ViewGroup bottomControlsViewGroup) { 22 | super( 23 | bottomControlsViewGroup, 24 | "speed_dialog_button", 25 | Settings.OVERLAY_BUTTON_SPEED_DIALOG, 26 | view -> VideoUtils.showPlaybackSpeedDialog(view.getContext()), 27 | view -> { 28 | if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() || 29 | VideoInformation.getPlaybackSpeed() == Settings.DEFAULT_PLAYBACK_SPEED.get()) { 30 | VideoInformation.overridePlaybackSpeed(1.0f); 31 | showToastShort(str("revanced_overlay_button_speed_dialog_reset", "1.0")); 32 | } else { 33 | float defaultSpeed = Settings.DEFAULT_PLAYBACK_SPEED.get(); 34 | VideoInformation.overridePlaybackSpeed(defaultSpeed); 35 | showToastShort(str("revanced_overlay_button_speed_dialog_reset", defaultSpeed)); 36 | } 37 | 38 | return true; 39 | } 40 | ); 41 | } 42 | 43 | /** 44 | * Injection point. 45 | */ 46 | public static void initialize(View bottomControlsViewGroup) { 47 | try { 48 | if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { 49 | instance = new SpeedDialog(viewGroup); 50 | } 51 | } catch (Exception ex) { 52 | Logger.printException(() -> "initialize failure", ex); 53 | } 54 | } 55 | 56 | /** 57 | * Injection point. 58 | */ 59 | public static void changeVisibility(boolean showing, boolean animation) { 60 | if (instance != null) instance.setVisibility(showing, animation); 61 | } 62 | 63 | public static void changeVisibilityNegatedImmediate() { 64 | if (instance != null) instance.setVisibilityNegatedImmediate(); 65 | } 66 | 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/settings/Settings.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.reddit.settings; 2 | 3 | import static java.lang.Boolean.FALSE; 4 | import static java.lang.Boolean.TRUE; 5 | 6 | import app.revanced.integrations.shared.settings.BaseSettings; 7 | import app.revanced.integrations.shared.settings.BooleanSetting; 8 | 9 | public class Settings extends BaseSettings { 10 | // Ads 11 | public static final BooleanSetting HIDE_COMMENT_ADS = new BooleanSetting("revanced_hide_comment_ads", TRUE, true); 12 | public static final BooleanSetting HIDE_OLD_POST_ADS = new BooleanSetting("revanced_hide_old_post_ads", TRUE, true); 13 | public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true); 14 | 15 | // Layout 16 | public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE, true); 17 | public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true); 18 | public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true); 19 | public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true); 20 | public static final BooleanSetting HIDE_GAMES_ON_REDDIT_SHELF = new BooleanSetting("revanced_hide_games_on_reddit_shelf", FALSE); 21 | public static final BooleanSetting HIDE_RECENTLY_VISITED_SHELF = new BooleanSetting("revanced_hide_recently_visited_shelf", FALSE); 22 | public static final BooleanSetting HIDE_REDDIT_PRO_SHELF = new BooleanSetting("revanced_hide_reddit_pro_shelf", FALSE); 23 | public static final BooleanSetting HIDE_RECOMMENDED_COMMUNITIES_SHELF = new BooleanSetting("revanced_hide_recommended_communities_shelf", FALSE, true); 24 | public static final BooleanSetting HIDE_TOOLBAR_BUTTON = new BooleanSetting("revanced_hide_toolbar_button", FALSE, true); 25 | public static final BooleanSetting HIDE_TRENDING_TODAY_SHELF = new BooleanSetting("revanced_hide_trending_today_shelf", FALSE, true); 26 | public static final BooleanSetting REMOVE_NSFW_DIALOG = new BooleanSetting("revanced_remove_nsfw_dialog", FALSE, true); 27 | public static final BooleanSetting REMOVE_NOTIFICATION_DIALOG = new BooleanSetting("revanced_remove_notification_dialog", FALSE, true); 28 | 29 | // Miscellaneous 30 | public static final BooleanSetting OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_open_links_directly", TRUE); 31 | public static final BooleanSetting OPEN_LINKS_EXTERNALLY = new BooleanSetting("revanced_open_links_externally", TRUE); 32 | public static final BooleanSetting SANITIZE_URL_QUERY = new BooleanSetting("revanced_sanitize_url_query", TRUE); 33 | } 34 | --------------------------------------------------------------------------------