├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── config.yml ├── dependabot.yml └── workflows │ ├── build_pull_request.yml │ ├── open_pull_request.yml │ └── release.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── app │ └── revanced │ └── integrations │ ├── all │ ├── connectivity │ │ └── wifi │ │ │ └── spoof │ │ │ └── SpoofWifiPatch.java │ ├── screencapture │ │ └── removerestriction │ │ │ └── RemoveScreencaptureRestrictionPatch.java │ └── screenshot │ │ └── removerestriction │ │ └── RemoveScreenshotRestrictionPatch.java │ ├── boostforreddit │ └── FixSLinksPatch.java │ ├── reddit │ └── patches │ │ └── FilterPromotedLinksPatch.java │ ├── shared │ ├── GmsCoreSupport.java │ ├── Logger.java │ ├── StringRef.java │ ├── Utils.java │ ├── checks │ │ ├── Check.java │ │ ├── CheckEnvironmentPatch.java │ │ └── PatchInfo.java │ ├── fixes │ │ └── slink │ │ │ ├── BaseFixSLinksPatch.java │ │ │ └── ResolveResult.java │ └── settings │ │ ├── BaseSettings.java │ │ ├── BooleanSetting.java │ │ ├── EnumSetting.java │ │ ├── FloatSetting.java │ │ ├── IntegerSetting.java │ │ ├── LongSetting.java │ │ ├── Setting.java │ │ ├── StringSetting.java │ │ └── preference │ │ ├── AbstractPreferenceFragment.java │ │ ├── ImportExportPreference.java │ │ ├── ReVancedAboutPreference.java │ │ ├── ResettableEditTextPreference.java │ │ └── SharedPrefCategory.java │ ├── syncforreddit │ ├── FixRedditVideoDownloadPatch.java │ └── FixSLinksPatch.java │ ├── tiktok │ ├── Utils.java │ ├── cleardisplay │ │ └── RememberClearDisplayPatch.java │ ├── download │ │ └── DownloadsPatch.java │ ├── feedfilter │ │ ├── AdsFilter.java │ │ ├── FeedItemsFilter.java │ │ ├── IFilter.java │ │ ├── ImageVideoFilter.java │ │ ├── LikeCountFilter.java │ │ ├── LiveFilter.java │ │ ├── StoryFilter.java │ │ └── ViewCountFilter.java │ ├── settings │ │ ├── AdPersonalizationActivityHook.java │ │ ├── Settings.java │ │ ├── SettingsStatus.java │ │ └── preference │ │ │ ├── DownloadPathPreference.java │ │ │ ├── InputTextPreference.java │ │ │ ├── RangeValuePreference.java │ │ │ ├── ReVancedPreferenceFragment.java │ │ │ ├── TogglePreference.java │ │ │ └── categories │ │ │ ├── ConditionalPreferenceCategory.java │ │ │ ├── DownloadsPreferenceCategory.java │ │ │ ├── FeedFilterPreferenceCategory.java │ │ │ ├── IntegrationsPreferenceCategory.java │ │ │ └── SimSpoofPreferenceCategory.java │ ├── speed │ │ └── PlaybackSpeedPatch.java │ └── spoof │ │ └── sim │ │ └── SpoofSimPatch.java │ ├── tudortmund │ └── lockscreen │ │ └── ShowOnLockscreenPatch.java │ ├── tumblr │ └── patches │ │ └── TimelineFilterPatch.java │ ├── twitch │ ├── Utils.java │ ├── adblock │ │ ├── IAdblockService.java │ │ ├── LuminousService.java │ │ └── PurpleAdblockService.java │ ├── api │ │ ├── PurpleAdblockApi.java │ │ ├── RequestInterceptor.java │ │ └── RetrofitClient.java │ ├── patches │ │ ├── AudioAdsPatch.java │ │ ├── AutoClaimChannelPointsPatch.java │ │ ├── DebugModePatch.java │ │ ├── EmbeddedAdsPatch.java │ │ ├── ShowDeletedMessagesPatch.java │ │ └── VideoAdsPatch.java │ └── settings │ │ ├── AppCompatActivityHook.java │ │ ├── Settings.java │ │ └── preference │ │ ├── CustomPreferenceCategory.java │ │ └── ReVancedPreferenceFragment.java │ ├── twitter │ ├── patches │ │ ├── hook │ │ │ ├── json │ │ │ │ ├── BaseJsonHook.kt │ │ │ │ ├── JsonHook.kt │ │ │ │ └── JsonHookPatch.kt │ │ │ ├── patch │ │ │ │ ├── Hook.kt │ │ │ │ ├── ads │ │ │ │ │ └── AdsHook.kt │ │ │ │ ├── dummy │ │ │ │ │ └── DummyHook.kt │ │ │ │ └── recommendation │ │ │ │ │ └── RecommendedUsersHook.kt │ │ │ └── twifucker │ │ │ │ ├── TwiFucker.kt │ │ │ │ └── TwiFuckerUtils.kt │ │ └── links │ │ │ ├── ChangeLinkSharingDomainPatch.java │ │ │ └── OpenLinksWithAppChooserPatch.java │ └── utils │ │ ├── json │ │ └── JsonUtils.kt │ │ └── stream │ │ └── StreamUtils.kt │ └── youtube │ ├── ByteTrieSearch.java │ ├── Event.kt │ ├── StringTrieSearch.java │ ├── ThemeHelper.java │ ├── TrieSearch.java │ ├── patches │ ├── AlternativeThumbnailsPatch.java │ ├── AutoRepeatPatch.java │ ├── BackgroundPlaybackPatch.java │ ├── BypassImageRegionRestrictionsPatch.java │ ├── BypassURLRedirectsPatch.java │ ├── ChangeStartPagePatch.java │ ├── CheckWatchHistoryDomainNameResolutionPatch.java │ ├── CopyVideoUrlPatch.java │ ├── CustomPlayerOverlayOpacityPatch.java │ ├── DisableAutoCaptionsPatch.java │ ├── DisableFullscreenAmbientModePatch.java │ ├── DisablePlayerPopupPanelsPatch.java │ ├── DisablePreciseSeekingGesturePatch.java │ ├── DisableResumingStartupShortsPlayerPatch.java │ ├── DisableRollingNumberAnimationsPatch.java │ ├── DisableSuggestedVideoEndScreenPatch.java │ ├── DownloadsPatch.java │ ├── FixBackToExitGesturePatch.java │ ├── FullscreenPanelsRemoverPatch.java │ ├── HDRAutoBrightnessPatch.java │ ├── HideAlbumCardsPatch.java │ ├── HideAutoplayButtonPatch.java │ ├── HideCaptionsButtonPatch.java │ ├── HideCastButtonPatch.java │ ├── HideCrowdfundingBoxPatch.java │ ├── HideEmailAddressPatch.java │ ├── HideEndscreenCardsPatch.java │ ├── HideFilterBarPatch.java │ ├── HideFloatingMicrophoneButtonPatch.java │ ├── HideGetPremiumPatch.java │ ├── HideInfoCardsPatch.java │ ├── HidePlayerButtonsPatch.java │ ├── HideSeekbarPatch.java │ ├── HideTimestampPatch.java │ ├── MiniplayerPatch.java │ ├── NavigationButtonsPatch.java │ ├── OpenLinksExternallyPatch.java │ ├── PlayerControlsPatch.java │ ├── PlayerOverlaysHookPatch.java │ ├── PlayerTypeHookPatch.java │ ├── RemoveTrackingQueryParameterPatch.java │ ├── RemoveViewerDiscretionDialogPatch.java │ ├── RestoreOldSeekbarThumbnailsPatch.java │ ├── ReturnYouTubeDislikePatch.java │ ├── SeekbarTappingPatch.java │ ├── SlideToSeekPatch.java │ ├── TabletLayoutPatch.java │ ├── VersionCheckPatch.java │ ├── VideoAdsPatch.java │ ├── VideoInformation.java │ ├── WideSearchbarPatch.java │ ├── ZoomHapticsPatch.java │ ├── announcements │ │ ├── AnnouncementsPatch.java │ │ └── requests │ │ │ └── AnnouncementsRoutes.java │ ├── components │ │ ├── AdsFilter.java │ │ ├── ButtonsFilter.java │ │ ├── CommentsFilter.java │ │ ├── CustomFilter.java │ │ ├── DescriptionComponentsFilter.java │ │ ├── Filter.java │ │ ├── FilterGroup.java │ │ ├── FilterGroupList.java │ │ ├── HideInfoCardsFilterPatch.java │ │ ├── KeywordContentFilter.java │ │ ├── LayoutComponentsFilter.java │ │ ├── LithoFilterPatch.java │ │ ├── PlaybackSpeedMenuFilterPatch.java │ │ ├── PlayerFlyoutMenuItemsFilter.java │ │ ├── ReturnYouTubeDislikeFilterPatch.java │ │ ├── ShortsFilter.java │ │ └── VideoQualityMenuFilterPatch.java │ ├── playback │ │ ├── quality │ │ │ ├── RememberVideoQualityPatch.java │ │ │ └── RestoreOldVideoQualityMenuPatch.java │ │ └── speed │ │ │ ├── CustomPlaybackSpeedPatch.java │ │ │ └── RememberPlaybackSpeedPatch.java │ ├── spoof │ │ ├── ClientType.java │ │ ├── DeviceHardwareSupport.java │ │ ├── SpoofAppVersionPatch.java │ │ ├── SpoofDeviceDimensionsPatch.java │ │ ├── SpoofVideoStreamsPatch.java │ │ └── requests │ │ │ ├── PlayerRoutes.java │ │ │ └── StreamingDataRequest.java │ └── theme │ │ ├── ProgressBarDrawable.java │ │ ├── SeekbarColorPatch.java │ │ └── ThemePatch.java │ ├── requests │ ├── Requester.java │ └── Route.java │ ├── returnyoutubedislike │ ├── ReturnYouTubeDislike.java │ └── requests │ │ ├── RYDVoteData.java │ │ ├── ReturnYouTubeDislikeApi.java │ │ └── ReturnYouTubeDislikeRoutes.java │ ├── settings │ ├── LicenseActivityHook.java │ ├── Settings.java │ └── preference │ │ ├── AlternativeThumbnailsAboutDeArrowPreference.java │ │ ├── ForceAVCSpoofingPreference.java │ │ ├── HtmlPreference.java │ │ ├── ReVancedPreferenceFragment.java │ │ ├── ReVancedYouTubeAboutPreference.java │ │ ├── ReturnYouTubeDislikePreferenceFragment.java │ │ └── SponsorBlockPreferenceFragment.java │ ├── shared │ ├── NavigationBar.java │ ├── PlayerControlsVisibilityObserver.kt │ ├── PlayerOverlays.kt │ ├── PlayerType.kt │ └── VideoState.kt │ ├── sponsorblock │ ├── SegmentPlaybackController.java │ ├── SponsorBlockSettings.java │ ├── SponsorBlockUtils.java │ ├── objects │ │ ├── CategoryBehaviour.java │ │ ├── SegmentCategory.java │ │ ├── SegmentCategoryListPreference.java │ │ ├── SponsorSegment.java │ │ └── UserStats.java │ ├── requests │ │ ├── SBRequester.java │ │ └── SBRoutes.java │ └── ui │ │ ├── CreateSegmentButtonController.java │ │ ├── NewSegmentLayout.java │ │ ├── SkipSponsorButton.java │ │ ├── SponsorBlockViewController.java │ │ └── VotingButtonController.java │ ├── swipecontrols │ ├── SwipeControlsConfigurationProvider.kt │ ├── SwipeControlsHostActivity.kt │ ├── controller │ │ ├── AudioVolumeController.kt │ │ ├── ScreenBrightnessController.kt │ │ ├── SwipeZonesController.kt │ │ ├── VolumeKeysController.kt │ │ └── gesture │ │ │ ├── ClassicSwipeController.kt │ │ │ ├── PressToSwipeController.kt │ │ │ └── core │ │ │ ├── BaseGestureController.kt │ │ │ ├── GestureController.kt │ │ │ ├── SwipeDetector.kt │ │ │ └── VolumeAndBrightnessScroller.kt │ ├── misc │ │ ├── Point.kt │ │ ├── Rectangle.kt │ │ ├── ScrollDistanceHelper.kt │ │ ├── SwipeControlsOverlay.kt │ │ └── SwipeControlsUtils.kt │ └── views │ │ └── SwipeControlsOverlayLayout.kt │ └── videoplayer │ ├── CopyVideoUrlButton.java │ ├── CopyVideoUrlTimestampButton.java │ ├── ExternalDownloadButton.java │ ├── PlaybackSpeedDialogButton.java │ └── PlayerControlButton.java ├── assets ├── revanced-headline │ ├── revanced-headline-vertical-dark.svg │ └── revanced-headline-vertical-light.svg └── revanced-logo │ └── revanced-logo.svg ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── package-lock.json ├── package.json ├── settings.gradle.kts └── stub ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java ├── android └── support │ ├── constraint │ └── ConstraintLayout.java │ └── v7 │ └── widget │ └── RecyclerView.java ├── com ├── bytedance │ └── ies │ │ └── ugc │ │ └── aweme │ │ └── commercialize │ │ └── compliance │ │ └── personalization │ │ └── AdPersonalizationActivity.java ├── google │ ├── android │ │ ├── apps │ │ │ └── youtube │ │ │ │ └── app │ │ │ │ └── ui │ │ │ │ └── SlimMetadataScrollableButtonContainerLayout.java │ │ └── libraries │ │ │ └── youtube │ │ │ └── rendering │ │ │ └── ui │ │ │ └── pivotbar │ │ │ └── PivotBar.java │ └── protos │ │ └── youtube │ │ └── api │ │ └── innertube │ │ └── InnertubeContext$ClientInfo.java ├── laurencedawson │ └── reddit_sync │ │ └── ui │ │ └── activities │ │ └── WebViewActivity.java ├── reddit │ └── domain │ │ └── model │ │ └── ILink.java ├── rubenmayayo │ └── reddit │ │ └── ui │ │ └── activities │ │ └── WebViewActivity.java ├── ss │ └── android │ │ └── ugc │ │ └── aweme │ │ └── feed │ │ └── model │ │ ├── Aweme.java │ │ ├── AwemeStatistics.java │ │ └── FeedItemList.java └── tumblr │ └── rumblr │ └── model │ ├── TimelineObject.java │ ├── TimelineObjectType.java │ └── Timelineable.java ├── org └── chromium │ └── net │ ├── UrlRequest.java │ ├── UrlResponseInfo.java │ └── impl │ └── CronetUrlRequest.java └── tv └── twitch └── android ├── feature └── settings │ └── menu │ └── SettingsMenuGroup.java ├── settings └── SettingsActivity.java └── shared └── chat └── util └── ClickableUsernameSpan.java /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_code_style = intellij_idea 3 | ktlint_standard_no-wildcard-imports = disabled -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | firstPRMergeComment: > 2 | Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution. 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | labels: [] 5 | directory: / 6 | target-branch: dev 7 | schedule: 8 | interval: monthly 9 | 10 | - package-ecosystem: npm 11 | labels: [] 12 | directory: / 13 | target-branch: dev 14 | schedule: 15 | interval: monthly 16 | 17 | - package-ecosystem: gradle 18 | labels: [] 19 | directory: / 20 | target-branch: dev 21 | schedule: 22 | interval: monthly 23 | -------------------------------------------------------------------------------- /.github/workflows/build_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Build pull request 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | 9 | jobs: 10 | release: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Cache Gradle 20 | uses: burrunan/gradle-cache-action@v1 21 | 22 | - name: Setup Java 23 | run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV 24 | 25 | - name: Build 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: ./gradlew build --no-daemon 29 | -------------------------------------------------------------------------------- /.github/workflows/open_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Open a PR to main 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | workflow_dispatch: 8 | 9 | env: 10 | MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main` 11 | 12 | jobs: 13 | pull-request: 14 | name: Open pull request 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Open pull request 21 | uses: repo-sync/pull-request@v2 22 | with: 23 | destination_branch: 'main' 24 | pr_title: 'chore: ${{ env.MESSAGE }}' 25 | pr_body: | 26 | This pull request will ${{ env.MESSAGE }}. 27 | 28 | ## Before merging this PR 29 | 30 | - [ ] Remember about https://github.com/revanced/revanced-patches 31 | pr_draft: true 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - dev 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | permissions: 14 | contents: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | # Make sure the release step uses its own credentials: 21 | # https://github.com/cycjimmy/semantic-release-action#private-packages 22 | persist-credentials: false 23 | fetch-depth: 0 24 | 25 | - name: Cache Gradle 26 | uses: burrunan/gradle-cache-action@v1 27 | 28 | - name: Setup Java 29 | run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV 30 | 31 | - name: Build 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: ./gradlew build clean 35 | 36 | - name: Setup Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: "lts/*" 40 | cache: 'npm' 41 | 42 | - name: Install dependencies 43 | run: npm install 44 | 45 | - name: Import GPG key 46 | uses: crazy-max/ghaction-import-gpg@v6 47 | with: 48 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 49 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 50 | fingerprint: ${{ vars.GPG_FINGERPRINT }} 51 | 52 | - name: Release 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: npm exec semantic-release 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.vscode 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | node_modules 13 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "dev", 6 | "prerelease": true 7 | } 8 | ], 9 | "plugins": [ 10 | [ 11 | "@semantic-release/commit-analyzer", { 12 | "releaseRules": [ 13 | { "type": "build", "scope": "Needs bump", "release": "patch" } 14 | ] 15 | } 16 | ], 17 | "@semantic-release/release-notes-generator", 18 | "@semantic-release/changelog", 19 | "gradle-semantic-release-plugin", 20 | [ 21 | "@semantic-release/git", 22 | { 23 | "assets": [ 24 | "CHANGELOG.md", 25 | "gradle.properties" 26 | ], 27 | "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 28 | } 29 | ], 30 | [ 31 | "@semantic-release/github", 32 | { 33 | "assets": [ 34 | { 35 | "path": "app/build/outputs/apk/release/revanced-integrations*" 36 | } 37 | ], 38 | successComment: false 39 | } 40 | ], 41 | [ 42 | "@saithodev/semantic-release-backmerge", 43 | { 44 | backmergeBranches: [{"from": "main", "to": "dev"}], 45 | clearWorkspace: true 46 | } 47 | ] 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔩 ReVanced Integrations 2 | 3 | The official ReVanced Integrations containing classes to be merged by ReVanced Patcher. 4 | 5 | ## ❓ How to use debugging: 6 | 7 | - Usage on Windows: ```adb logcat | findstr "revanced" > log.txt``` 8 | - Usage on Linux: ```adb logcat | grep --line-buffered "revanced" > log.txt``` 9 | 10 | This will write the log to a file called log.txt which you can view then. 11 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin) 4 | publishing 5 | signing 6 | } 7 | 8 | android { 9 | namespace = "app.revanced.integrations" 10 | compileSdk = 33 11 | 12 | applicationVariants.all { 13 | outputs.all { 14 | this as com.android.build.gradle.internal.api.ApkVariantOutputImpl 15 | 16 | outputFileName = "${rootProject.name}-$versionName.apk" 17 | } 18 | } 19 | 20 | defaultConfig { 21 | applicationId = "app.revanced.integrations" 22 | minSdk = 23 23 | targetSdk = 33 24 | multiDexEnabled = false 25 | versionName = version as String 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = true 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro", 34 | ) 35 | } 36 | } 37 | 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_11 40 | targetCompatibility = JavaVersion.VERSION_11 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = JavaVersion.VERSION_11.toString() 45 | } 46 | } 47 | 48 | dependencies { 49 | compileOnly(libs.appcompat) 50 | compileOnly(libs.annotation) 51 | compileOnly(libs.okhttp) 52 | compileOnly(libs.retrofit) 53 | 54 | compileOnly(project(":stub")) 55 | } 56 | 57 | 58 | tasks { 59 | val assembleReleaseSignApk by registering { 60 | dependsOn("assembleRelease") 61 | 62 | val apk = layout.buildDirectory.file("outputs/apk/release/${rootProject.name}-$version.apk") 63 | 64 | inputs.file(apk).withPropertyName("input") 65 | outputs.file(apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") }) 66 | 67 | doLast { 68 | signing { 69 | useGpgCmd() 70 | sign(*inputs.files.files.toTypedArray()) 71 | } 72 | } 73 | } 74 | 75 | // Needed by gradle-semantic-release-plugin. 76 | // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435. 77 | publish { 78 | dependsOn(assembleReleaseSignApk) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.all.screencapture.removerestriction; 2 | 3 | import android.media.AudioAttributes; 4 | import android.os.Build; 5 | 6 | import androidx.annotation.RequiresApi; 7 | 8 | public final class RemoveScreencaptureRestrictionPatch { 9 | // Member of AudioAttributes.Builder 10 | @RequiresApi(api = Build.VERSION_CODES.Q) 11 | public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) { 12 | builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL); 13 | 14 | return builder; 15 | } 16 | 17 | // Member of AudioManager static class 18 | public static void setAllowedCapturePolicy(final int capturePolicy) { 19 | // Ignore request 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.all.screenshot.removerestriction; 2 | 3 | import android.view.Window; 4 | import android.view.WindowManager; 5 | 6 | public class RemoveScreenshotRestrictionPatch { 7 | 8 | public static void addFlags(Window window, int flags) { 9 | window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE); 10 | } 11 | 12 | public static void setFlags(Window window, int flags, int mask) { 13 | window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/boostforreddit/FixSLinksPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.boostforreddit; 2 | 3 | import com.rubenmayayo.reddit.ui.activities.WebViewActivity; 4 | 5 | import app.revanced.integrations.shared.fixes.slink.BaseFixSLinksPatch; 6 | 7 | /** @noinspection unused*/ 8 | public class FixSLinksPatch extends BaseFixSLinksPatch { 9 | static { 10 | INSTANCE = new FixSLinksPatch(); 11 | } 12 | 13 | private FixSLinksPatch() { 14 | webViewActivityClass = WebViewActivity.class; 15 | } 16 | 17 | public static boolean patchResolveSLink(String link) { 18 | return INSTANCE.resolveSLink(link); 19 | } 20 | 21 | public static void patchSetAccessToken(String accessToken) { 22 | INSTANCE.setAccessToken(accessToken); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/reddit/patches/FilterPromotedLinksPatch.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 | 8 | public final class FilterPromotedLinksPatch { 9 | /** 10 | * Filters list from promoted links. 11 | **/ 12 | public static List filterChildren(final Iterable links) { 13 | final List filteredList = new ArrayList<>(); 14 | 15 | for (Object item : links) { 16 | if (item instanceof ILink && ((ILink) item).getPromoted()) continue; 17 | 18 | filteredList.add(item); 19 | } 20 | 21 | return filteredList; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.checks; 2 | 3 | // Fields are set by the patch. Do not modify. 4 | // Fields are not final, because the compiler is inlining them. 5 | final class PatchInfo { 6 | static long PATCH_TIME = 0L; 7 | 8 | final static class Build { 9 | static String PATCH_BOARD = ""; 10 | static String PATCH_BOOTLOADER = ""; 11 | static String PATCH_BRAND = ""; 12 | static String PATCH_CPU_ABI = ""; 13 | static String PATCH_CPU_ABI2 = ""; 14 | static String PATCH_DEVICE = ""; 15 | static String PATCH_DISPLAY = ""; 16 | static String PATCH_FINGERPRINT = ""; 17 | static String PATCH_HARDWARE = ""; 18 | static String PATCH_HOST = ""; 19 | static String PATCH_ID = ""; 20 | static String PATCH_MANUFACTURER = ""; 21 | static String PATCH_MODEL = ""; 22 | static String PATCH_PRODUCT = ""; 23 | static String PATCH_RADIO = ""; 24 | static String PATCH_TAGS = ""; 25 | static String PATCH_TYPE = ""; 26 | static String PATCH_USER = ""; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/fixes/slink/ResolveResult.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.fixes.slink; 2 | 3 | public enum ResolveResult { 4 | // Let app handle rest of stuff 5 | CONTINUE, 6 | // Start app, to make it cache its access_token 7 | ACCESS_TOKEN_START, 8 | // Don't do anything - we started resolving 9 | DO_NOTHING 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/BaseSettings.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings; 2 | 3 | import static java.lang.Boolean.FALSE; 4 | import static java.lang.Boolean.TRUE; 5 | import static app.revanced.integrations.shared.settings.Setting.parent; 6 | 7 | /** 8 | * Settings shared across multiple apps. 9 | * 10 | * To ensure this class is loaded when the UI is created, app specific setting bundles should extend 11 | * or reference this class. 12 | */ 13 | public class BaseSettings { 14 | public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE); 15 | public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG)); 16 | public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message"); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/FloatSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class FloatSetting extends Setting { 13 | 14 | public FloatSetting(String key, Float defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public FloatSetting(String key, Float defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public FloatSetting(String key, Float defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public FloatSetting(String key, Float defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getFloatString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return (float) json.getDouble(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Float.valueOf(Objects.requireNonNull(newValue)); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull Float newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveFloatString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Float get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/IntegerSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class IntegerSetting extends Setting { 13 | 14 | public IntegerSetting(String key, Integer defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public IntegerSetting(String key, Integer defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getIntegerString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return json.getInt(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Integer.valueOf(Objects.requireNonNull(newValue)); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull Integer newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveIntegerString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Integer get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/LongSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class LongSetting extends Setting { 13 | 14 | public LongSetting(String key, Long defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public LongSetting(String key, Long defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public LongSetting(String key, Long defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public LongSetting(String key, Long defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getLongString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return json.getLong(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Long.valueOf(Objects.requireNonNull(newValue)); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull Long newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveLongString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Long get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/StringSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class StringSetting extends Setting { 13 | 14 | public StringSetting(String key, String defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public StringSetting(String key, String defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public StringSetting(String key, String defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public StringSetting(String key, String defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return json.getString(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Objects.requireNonNull(newValue); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull String newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public String get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.shared.settings.preference; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.preference.EditTextPreference; 7 | import android.util.AttributeSet; 8 | import android.widget.Button; 9 | import android.widget.EditText; 10 | 11 | import app.revanced.integrations.shared.Utils; 12 | import app.revanced.integrations.shared.settings.Setting; 13 | import app.revanced.integrations.shared.Logger; 14 | 15 | import java.util.Objects; 16 | 17 | import static app.revanced.integrations.shared.StringRef.str; 18 | 19 | @SuppressWarnings({"unused", "deprecation"}) 20 | public class ResettableEditTextPreference extends EditTextPreference { 21 | 22 | public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 23 | super(context, attrs, defStyleAttr, defStyleRes); 24 | } 25 | public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { 26 | super(context, attrs, defStyleAttr); 27 | } 28 | public ResettableEditTextPreference(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | } 31 | public ResettableEditTextPreference(Context context) { 32 | super(context); 33 | } 34 | 35 | @Override 36 | protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { 37 | super.onPrepareDialogBuilder(builder); 38 | Utils.setEditTextDialogTheme(builder); 39 | 40 | Setting setting = Setting.getSettingFromPath(getKey()); 41 | if (setting != null) { 42 | builder.setNeutralButton(str("revanced_settings_reset"), null); 43 | } 44 | } 45 | 46 | @Override 47 | protected void showDialog(Bundle state) { 48 | super.showDialog(state); 49 | 50 | // Override the button click listener to prevent dismissing the dialog. 51 | Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL); 52 | if (button == null) { 53 | return; 54 | } 55 | button.setOnClickListener(v -> { 56 | try { 57 | Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); 58 | String defaultStringValue = setting.defaultValue.toString(); 59 | EditText editText = getEditText(); 60 | editText.setText(defaultStringValue); 61 | editText.setSelection(defaultStringValue.length()); // move cursor to end of text 62 | } catch (Exception ex) { 63 | Logger.printException(() -> "reset failure", ex); 64 | } 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/syncforreddit/FixRedditVideoDownloadPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.syncforreddit; 2 | 3 | import android.util.Pair; 4 | import androidx.annotation.Nullable; 5 | import org.w3c.dom.Element; 6 | import org.xml.sax.SAXException; 7 | 8 | import javax.xml.parsers.DocumentBuilderFactory; 9 | import javax.xml.parsers.ParserConfigurationException; 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | 15 | /** 16 | * @noinspection unused 17 | */ 18 | public class FixRedditVideoDownloadPatch { 19 | private static @Nullable Pair getBestMpEntry(Element element) { 20 | var representations = element.getElementsByTagName("Representation"); 21 | var entries = new ArrayList>(); 22 | 23 | for (int i = 0; i < representations.getLength(); i++) { 24 | Element representation = (Element) representations.item(i); 25 | var bandwidthStr = representation.getAttribute("bandwidth"); 26 | try { 27 | var bandwidth = Integer.parseInt(bandwidthStr); 28 | var baseUrl = representation.getElementsByTagName("BaseURL").item(0); 29 | if (baseUrl != null) { 30 | entries.add(new Pair<>(bandwidth, baseUrl.getTextContent())); 31 | } 32 | } catch (NumberFormatException ignored) { 33 | } 34 | } 35 | 36 | if (entries.isEmpty()) { 37 | return null; 38 | } 39 | 40 | Collections.sort(entries, (e1, e2) -> e2.first - e1.first); 41 | return entries.get(0); 42 | } 43 | 44 | private static String[] parse(byte[] data) throws ParserConfigurationException, IOException, SAXException { 45 | var adaptionSets = DocumentBuilderFactory 46 | .newInstance() 47 | .newDocumentBuilder() 48 | .parse(new ByteArrayInputStream(data)) 49 | .getElementsByTagName("AdaptationSet"); 50 | 51 | String videoUrl = null; 52 | String audioUrl = null; 53 | 54 | for (int i = 0; i < adaptionSets.getLength(); i++) { 55 | Element element = (Element) adaptionSets.item(i); 56 | var contentType = element.getAttribute("contentType"); 57 | var bestEntry = getBestMpEntry(element); 58 | if (bestEntry == null) continue; 59 | 60 | if (contentType.equalsIgnoreCase("video")) { 61 | videoUrl = bestEntry.second; 62 | } else if (contentType.equalsIgnoreCase("audio")) { 63 | audioUrl = bestEntry.second; 64 | } 65 | } 66 | 67 | return new String[]{videoUrl, audioUrl}; 68 | } 69 | 70 | public static String[] getLinks(byte[] data) { 71 | try { 72 | return parse(data); 73 | } catch (ParserConfigurationException | IOException | SAXException e) { 74 | return new String[]{null, null}; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/syncforreddit/FixSLinksPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.syncforreddit; 2 | 3 | import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity; 4 | 5 | import app.revanced.integrations.shared.fixes.slink.BaseFixSLinksPatch; 6 | 7 | /** @noinspection unused*/ 8 | public class FixSLinksPatch extends BaseFixSLinksPatch { 9 | static { 10 | INSTANCE = new FixSLinksPatch(); 11 | } 12 | 13 | private FixSLinksPatch() { 14 | webViewActivityClass = WebViewActivity.class; 15 | } 16 | 17 | public static boolean patchResolveSLink(String link) { 18 | return INSTANCE.resolveSLink(link); 19 | } 20 | 21 | public static void patchSetAccessToken(String accessToken) { 22 | INSTANCE.setAccessToken(accessToken); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/Utils.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok; 2 | 3 | import app.revanced.integrations.shared.settings.StringSetting; 4 | 5 | public class Utils { 6 | 7 | // Edit: This could be handled using a custom Setting class 8 | // that saves its value to preferences and JSON using the formatted String created here. 9 | public static long[] parseMinMax(StringSetting setting) { 10 | final String[] minMax = setting.get().split("-"); 11 | if (minMax.length == 2) { 12 | try { 13 | final long min = Long.parseLong(minMax[0]); 14 | final long max = Long.parseLong(minMax[1]); 15 | 16 | if (min <= max && min >= 0) return new long[]{min, max}; 17 | 18 | } catch (NumberFormatException ignored) { 19 | } 20 | } 21 | 22 | setting.save("0-" + Long.MAX_VALUE); 23 | return new long[]{0L, Long.MAX_VALUE}; 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/cleardisplay/RememberClearDisplayPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.cleardisplay; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class RememberClearDisplayPatch { 7 | public static boolean getClearDisplayState() { 8 | return Settings.CLEAR_DISPLAY.get(); 9 | } 10 | public static void rememberClearDisplayState(boolean newState) { 11 | Settings.CLEAR_DISPLAY.save(newState); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/download/DownloadsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.download; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class DownloadsPatch { 7 | public static String getDownloadPath() { 8 | return Settings.DOWNLOAD_PATH.get(); 9 | } 10 | 11 | public static boolean shouldRemoveWatermark() { 12 | return Settings.DOWNLOAD_WATERMARK.get(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/AdsFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | import com.ss.android.ugc.aweme.feed.model.Aweme; 5 | 6 | public class AdsFilter implements IFilter { 7 | @Override 8 | public boolean getEnabled() { 9 | return Settings.REMOVE_ADS.get(); 10 | } 11 | 12 | @Override 13 | public boolean getFiltered(Aweme item) { 14 | return item.isAd() || item.isWithPromotionalMusic(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/FeedItemsFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import com.ss.android.ugc.aweme.feed.model.Aweme; 4 | import com.ss.android.ugc.aweme.feed.model.FeedItemList; 5 | 6 | import java.util.Iterator; 7 | import java.util.List; 8 | 9 | public final class FeedItemsFilter { 10 | private static final List FILTERS = List.of( 11 | new AdsFilter(), 12 | new LiveFilter(), 13 | new StoryFilter(), 14 | new ImageVideoFilter(), 15 | new ViewCountFilter(), 16 | new LikeCountFilter() 17 | ); 18 | 19 | public static void filter(FeedItemList feedItemList) { 20 | Iterator feedItemListIterator = feedItemList.items.iterator(); 21 | while (feedItemListIterator.hasNext()) { 22 | Aweme item = feedItemListIterator.next(); 23 | if (item == null) continue; 24 | 25 | for (IFilter filter : FILTERS) { 26 | boolean enabled = filter.getEnabled(); 27 | if (enabled && filter.getFiltered(item)) { 28 | feedItemListIterator.remove(); 29 | break; 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/IFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import com.ss.android.ugc.aweme.feed.model.Aweme; 4 | 5 | public interface IFilter { 6 | boolean getEnabled(); 7 | 8 | boolean getFiltered(Aweme item); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/ImageVideoFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | import com.ss.android.ugc.aweme.feed.model.Aweme; 5 | 6 | public class ImageVideoFilter implements IFilter { 7 | @Override 8 | public boolean getEnabled() { 9 | return Settings.HIDE_IMAGE.get(); 10 | } 11 | 12 | @Override 13 | public boolean getFiltered(Aweme item) { 14 | return item.isImage() || item.isPhotoMode(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/LikeCountFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | import com.ss.android.ugc.aweme.feed.model.Aweme; 5 | import com.ss.android.ugc.aweme.feed.model.AwemeStatistics; 6 | 7 | import static app.revanced.integrations.tiktok.Utils.parseMinMax; 8 | 9 | public final class LikeCountFilter implements IFilter { 10 | final long minLike; 11 | final long maxLike; 12 | 13 | LikeCountFilter() { 14 | long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES); 15 | minLike = minMax[0]; 16 | maxLike = minMax[1]; 17 | } 18 | 19 | @Override 20 | public boolean getEnabled() { 21 | return true; 22 | } 23 | 24 | @Override 25 | public boolean getFiltered(Aweme item) { 26 | AwemeStatistics statistics = item.getStatistics(); 27 | if (statistics == null) return false; 28 | 29 | long likeCount = statistics.getDiggCount(); 30 | return likeCount < minLike || likeCount > maxLike; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/LiveFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | import com.ss.android.ugc.aweme.feed.model.Aweme; 5 | 6 | public class LiveFilter implements IFilter { 7 | @Override 8 | public boolean getEnabled() { 9 | return Settings.HIDE_LIVE.get(); 10 | } 11 | 12 | @Override 13 | public boolean getFiltered(Aweme item) { 14 | return item.isLive() || item.isLiveReplay(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/StoryFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | import com.ss.android.ugc.aweme.feed.model.Aweme; 5 | 6 | public class StoryFilter implements IFilter { 7 | @Override 8 | public boolean getEnabled() { 9 | return Settings.HIDE_STORY.get(); 10 | } 11 | 12 | @Override 13 | public boolean getFiltered(Aweme item) { 14 | return item.getIsTikTokStory(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/feedfilter/ViewCountFilter.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.feedfilter; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | import com.ss.android.ugc.aweme.feed.model.Aweme; 5 | import com.ss.android.ugc.aweme.feed.model.AwemeStatistics; 6 | 7 | import static app.revanced.integrations.tiktok.Utils.parseMinMax; 8 | 9 | public class ViewCountFilter implements IFilter { 10 | final long minView; 11 | final long maxView; 12 | 13 | ViewCountFilter() { 14 | long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS); 15 | minView = minMax[0]; 16 | maxView = minMax[1]; 17 | } 18 | 19 | @Override 20 | public boolean getEnabled() { 21 | return true; 22 | } 23 | 24 | @Override 25 | public boolean getFiltered(Aweme item) { 26 | AwemeStatistics statistics = item.getStatistics(); 27 | if (statistics == null) return false; 28 | 29 | long playCount = statistics.getPlayCount(); 30 | return playCount < minView || playCount > maxView; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/Settings.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.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 | import app.revanced.integrations.shared.settings.FloatSetting; 9 | import app.revanced.integrations.shared.settings.StringSetting; 10 | 11 | public class Settings extends BaseSettings { 12 | public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true); 13 | public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true); 14 | public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true); 15 | public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true); 16 | public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true); 17 | public static final StringSetting MIN_MAX_LIKES = new StringSetting("min_max_likes", "0-" + Long.MAX_VALUE, true); 18 | public static final StringSetting DOWNLOAD_PATH = new StringSetting("down_path", "DCIM/TikTok"); 19 | public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE); 20 | public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE); 21 | public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f); 22 | public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true); 23 | public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us"); 24 | public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160"); 25 | public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile"); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/SettingsStatus.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings; 2 | 3 | public class SettingsStatus { 4 | public static boolean feedFilterEnabled = false; 5 | public static boolean downloadEnabled = false; 6 | public static boolean simSpoofEnabled = false; 7 | 8 | public static void enableFeedFilter() { 9 | feedFilterEnabled = true; 10 | } 11 | 12 | public static void enableDownload() { 13 | downloadEnabled = true; 14 | } 15 | 16 | public static void enableSimSpoof() { 17 | simSpoofEnabled = true; 18 | } 19 | 20 | public static void load() { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/preference/InputTextPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.preference.EditTextPreference; 5 | 6 | import app.revanced.integrations.shared.settings.StringSetting; 7 | 8 | public class InputTextPreference extends EditTextPreference { 9 | 10 | public InputTextPreference(Context context, String title, String summary, StringSetting setting) { 11 | super(context); 12 | this.setTitle(title); 13 | this.setSummary(summary); 14 | this.setKey(setting.key); 15 | this.setText(setting.get()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/preference/ReVancedPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings.preference; 2 | 3 | import android.preference.Preference; 4 | import android.preference.PreferenceScreen; 5 | import androidx.annotation.NonNull; 6 | import app.revanced.integrations.shared.settings.Setting; 7 | import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment; 8 | import app.revanced.integrations.tiktok.settings.preference.categories.DownloadsPreferenceCategory; 9 | import app.revanced.integrations.tiktok.settings.preference.categories.FeedFilterPreferenceCategory; 10 | import app.revanced.integrations.tiktok.settings.preference.categories.IntegrationsPreferenceCategory; 11 | import app.revanced.integrations.tiktok.settings.preference.categories.SimSpoofPreferenceCategory; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | /** 15 | * Preference fragment for ReVanced settings 16 | */ 17 | @SuppressWarnings("deprecation") 18 | public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { 19 | 20 | @Override 21 | protected void syncSettingWithPreference(@NonNull @NotNull Preference pref, 22 | @NonNull @NotNull Setting setting, 23 | boolean applySettingToPreference) { 24 | if (pref instanceof RangeValuePreference) { 25 | RangeValuePreference rangeValuePref = (RangeValuePreference) pref; 26 | Setting.privateSetValueFromString(setting, rangeValuePref.getValue()); 27 | } else if (pref instanceof DownloadPathPreference) { 28 | DownloadPathPreference downloadPathPref = (DownloadPathPreference) pref; 29 | Setting.privateSetValueFromString(setting, downloadPathPref.getValue()); 30 | } else { 31 | super.syncSettingWithPreference(pref, setting, applySettingToPreference); 32 | } 33 | } 34 | 35 | @Override 36 | protected void initialize() { 37 | final var context = getContext(); 38 | 39 | // Currently no resources can be compiled for TikTok (fails with aapt error). 40 | // So all TikTok Strings are hard coded in integrations. 41 | restartDialogTitle = "Refresh and restart"; 42 | restartDialogButtonText = "Restart"; 43 | confirmDialogTitle = "Do you wish to proceed?"; 44 | 45 | PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); 46 | setPreferenceScreen(preferenceScreen); 47 | 48 | // Custom categories reference app specific Settings class. 49 | new FeedFilterPreferenceCategory(context, preferenceScreen); 50 | new DownloadsPreferenceCategory(context, preferenceScreen); 51 | new SimSpoofPreferenceCategory(context, preferenceScreen); 52 | new IntegrationsPreferenceCategory(context, preferenceScreen); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/preference/TogglePreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.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/tiktok/settings/preference/categories/ConditionalPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.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/tiktok/settings/preference/categories/DownloadsPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceScreen; 5 | import app.revanced.integrations.tiktok.settings.Settings; 6 | import app.revanced.integrations.tiktok.settings.SettingsStatus; 7 | import app.revanced.integrations.tiktok.settings.preference.DownloadPathPreference; 8 | import app.revanced.integrations.tiktok.settings.preference.TogglePreference; 9 | 10 | @SuppressWarnings("deprecation") 11 | public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory { 12 | public DownloadsPreferenceCategory(Context context, PreferenceScreen screen) { 13 | super(context, screen); 14 | setTitle("Downloads"); 15 | } 16 | 17 | @Override 18 | public boolean getSettingsStatus() { 19 | return SettingsStatus.downloadEnabled; 20 | } 21 | 22 | @Override 23 | public void addPreferences(Context context) { 24 | addPreference(new DownloadPathPreference( 25 | context, 26 | "Download path", 27 | Settings.DOWNLOAD_PATH 28 | )); 29 | addPreference(new TogglePreference( 30 | context, 31 | "Remove watermark", "", 32 | Settings.DOWNLOAD_WATERMARK 33 | )); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/preference/categories/FeedFilterPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceScreen; 5 | import app.revanced.integrations.tiktok.settings.preference.RangeValuePreference; 6 | import app.revanced.integrations.tiktok.settings.Settings; 7 | import app.revanced.integrations.tiktok.settings.SettingsStatus; 8 | import app.revanced.integrations.tiktok.settings.preference.TogglePreference; 9 | 10 | @SuppressWarnings("deprecation") 11 | public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory { 12 | public FeedFilterPreferenceCategory(Context context, PreferenceScreen screen) { 13 | super(context, screen); 14 | setTitle("Feed filter"); 15 | } 16 | 17 | @Override 18 | public boolean getSettingsStatus() { 19 | return SettingsStatus.feedFilterEnabled; 20 | } 21 | 22 | @Override 23 | public void addPreferences(Context context) { 24 | addPreference(new TogglePreference( 25 | context, 26 | "Remove feed ads", "Remove ads from feed.", 27 | Settings.REMOVE_ADS 28 | )); 29 | addPreference(new TogglePreference( 30 | context, 31 | "Hide livestreams", "Hide livestreams from feed.", 32 | Settings.HIDE_LIVE 33 | )); 34 | addPreference(new TogglePreference( 35 | context, 36 | "Hide story", "Hide story from feed.", 37 | Settings.HIDE_STORY 38 | )); 39 | addPreference(new TogglePreference( 40 | context, 41 | "Hide image video", "Hide image video from feed.", 42 | Settings.HIDE_IMAGE 43 | )); 44 | addPreference(new RangeValuePreference( 45 | context, 46 | "Min/Max views", "The minimum or maximum views of a video to show.", 47 | Settings.MIN_MAX_VIEWS 48 | )); 49 | addPreference(new RangeValuePreference( 50 | context, 51 | "Min/Max likes", "The minimum or maximum likes of a video to show.", 52 | Settings.MIN_MAX_LIKES 53 | )); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/preference/categories/IntegrationsPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceScreen; 5 | 6 | import app.revanced.integrations.shared.settings.BaseSettings; 7 | import app.revanced.integrations.tiktok.settings.preference.TogglePreference; 8 | 9 | @SuppressWarnings("deprecation") 10 | public class IntegrationsPreferenceCategory extends ConditionalPreferenceCategory { 11 | public IntegrationsPreferenceCategory(Context context, PreferenceScreen screen) { 12 | super(context, screen); 13 | setTitle("Integrations"); 14 | } 15 | 16 | @Override 17 | public boolean getSettingsStatus() { 18 | return true; 19 | } 20 | 21 | @Override 22 | public void addPreferences(Context context) { 23 | addPreference(new TogglePreference(context, 24 | "Enable debug log", 25 | "Show integration debug log.", 26 | BaseSettings.DEBUG 27 | )); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/settings/preference/categories/SimSpoofPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.settings.preference.categories; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceScreen; 5 | import app.revanced.integrations.tiktok.settings.Settings; 6 | import app.revanced.integrations.tiktok.settings.SettingsStatus; 7 | import app.revanced.integrations.tiktok.settings.preference.InputTextPreference; 8 | import app.revanced.integrations.tiktok.settings.preference.TogglePreference; 9 | 10 | @SuppressWarnings("deprecation") 11 | public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory { 12 | public SimSpoofPreferenceCategory(Context context, PreferenceScreen screen) { 13 | super(context, screen); 14 | setTitle("Bypass regional restriction"); 15 | } 16 | 17 | 18 | @Override 19 | public boolean getSettingsStatus() { 20 | return SettingsStatus.simSpoofEnabled; 21 | } 22 | 23 | @Override 24 | public void addPreferences(Context context) { 25 | addPreference(new TogglePreference( 26 | context, 27 | "Fake sim card info", 28 | "Bypass regional restriction by fake sim card information.", 29 | Settings.SIM_SPOOF 30 | )); 31 | addPreference(new InputTextPreference( 32 | context, 33 | "Country ISO", "us, uk, jp, ...", 34 | Settings.SIM_SPOOF_ISO 35 | )); 36 | addPreference(new InputTextPreference( 37 | context, 38 | "Operator mcc+mnc", "mcc+mnc", 39 | Settings.SIMSPOOF_MCCMNC 40 | )); 41 | addPreference(new InputTextPreference( 42 | context, 43 | "Operator name", "Name of the operator.", 44 | Settings.SIMSPOOF_OP_NAME 45 | )); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/speed/PlaybackSpeedPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.speed; 2 | 3 | import app.revanced.integrations.tiktok.settings.Settings; 4 | 5 | public class PlaybackSpeedPatch { 6 | public static void rememberPlaybackSpeed(float newSpeed) { 7 | Settings.REMEMBERED_SPEED.save(newSpeed); 8 | } 9 | 10 | public static float getPlaybackSpeed() { 11 | return Settings.REMEMBERED_SPEED.get(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tiktok/spoof/sim/SpoofSimPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tiktok.spoof.sim; 2 | 3 | import app.revanced.integrations.shared.Logger; 4 | import app.revanced.integrations.tiktok.settings.Settings; 5 | 6 | @SuppressWarnings("unused") 7 | public class SpoofSimPatch { 8 | 9 | private static final boolean ENABLED = Settings.SIM_SPOOF.get(); 10 | 11 | public static String getCountryIso(String value) { 12 | if (ENABLED) { 13 | String iso = Settings.SIM_SPOOF_ISO.get(); 14 | Logger.printDebug(() -> "Spoofing sim ISO from: " + value + " to: " + iso); 15 | return iso; 16 | } 17 | return value; 18 | } 19 | 20 | public static String getOperator(String value) { 21 | if (ENABLED) { 22 | String mcc_mnc = Settings.SIMSPOOF_MCCMNC.get(); 23 | Logger.printDebug(() -> "Spoofing sim MCC-MNC from: " + value + " to: " + mcc_mnc); 24 | return mcc_mnc; 25 | } 26 | return value; 27 | } 28 | 29 | public static String getOperatorName(String value) { 30 | if (ENABLED) { 31 | String operator = Settings.SIMSPOOF_OP_NAME.get(); 32 | Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator); 33 | return operator; 34 | } 35 | return value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tudortmund/lockscreen/ShowOnLockscreenPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tudortmund.lockscreen; 2 | 3 | import android.content.Context; 4 | import android.hardware.display.DisplayManager; 5 | import android.os.Build; 6 | import android.view.Display; 7 | import android.view.Window; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; 11 | import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; 12 | 13 | public class ShowOnLockscreenPatch { 14 | /** 15 | * @noinspection deprecation 16 | */ 17 | public static Window getWindow(AppCompatActivity activity, float brightness) { 18 | Window window = activity.getWindow(); 19 | 20 | if (brightness >= 0) { 21 | // High brightness set, therefore show on lockscreen. 22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(true); 23 | else window.addFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD); 24 | } else { 25 | // Ignore brightness reset when the screen is turned off. 26 | DisplayManager displayManager = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE); 27 | 28 | boolean isScreenOn = false; 29 | for (Display display : displayManager.getDisplays()) { 30 | if (display.getState() == Display.STATE_OFF) continue; 31 | 32 | isScreenOn = true; 33 | break; 34 | } 35 | 36 | if (isScreenOn) { 37 | // Hide on lockscreen. 38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(false); 39 | else window.clearFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD); 40 | } 41 | } 42 | 43 | return window; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/tumblr/patches/TimelineFilterPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.tumblr.patches; 2 | 3 | import com.tumblr.rumblr.model.TimelineObject; 4 | import com.tumblr.rumblr.model.Timelineable; 5 | 6 | import java.util.HashSet; 7 | import java.util.List; 8 | 9 | public final class TimelineFilterPatch { 10 | private static final HashSet blockedObjectTypes = new HashSet<>(); 11 | 12 | static { 13 | // This dummy gets removed by the TimelineFilterPatch and in its place, 14 | // equivalent instructions with a different constant string 15 | // will be inserted for each Timeline object type filter. 16 | // Modifying this line may break the patch. 17 | blockedObjectTypes.add("BLOCKED_OBJECT_DUMMY"); 18 | } 19 | 20 | // Calls to this method are injected where the list of Timeline objects is first received. 21 | // We modify the list filter out elements that we want to hide. 22 | public static void filterTimeline(final List> timelineObjects) { 23 | final var iterator = timelineObjects.iterator(); 24 | while (iterator.hasNext()) { 25 | var timelineElement = iterator.next(); 26 | if (timelineElement == null) continue; 27 | 28 | String elementType = timelineElement.getData().getTimelineObjectType().toString(); 29 | if (blockedObjectTypes.contains(elementType)) iterator.remove(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/Utils.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch; 2 | 3 | public class Utils { 4 | 5 | /* Called from SettingsPatch smali */ 6 | public static int getStringId(String name) { 7 | return app.revanced.integrations.shared.Utils.getResourceIdentifier(name, "string"); 8 | } 9 | 10 | /* Called from SettingsPatch smali */ 11 | public static int getDrawableId(String name) { 12 | return app.revanced.integrations.shared.Utils.getResourceIdentifier(name, "drawable"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/adblock/IAdblockService.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.adblock; 2 | 3 | import okhttp3.Request; 4 | 5 | public interface IAdblockService { 6 | String friendlyName(); 7 | 8 | Integer maxAttempts(); 9 | 10 | Boolean isAvailable(); 11 | 12 | Request rewriteHlsRequest(Request originalRequest); 13 | 14 | static boolean isVod(Request request){ 15 | return request.url().pathSegments().contains("vod"); 16 | } 17 | 18 | static String channelName(Request request) { 19 | for (String pathSegment : request.url().pathSegments()) { 20 | if (pathSegment.endsWith(".m3u8")) { 21 | return pathSegment.replace(".m3u8", ""); 22 | } 23 | } 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/adblock/LuminousService.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.adblock; 2 | 3 | import app.revanced.integrations.shared.Logger; 4 | import okhttp3.HttpUrl; 5 | import okhttp3.Request; 6 | 7 | import static app.revanced.integrations.shared.StringRef.str; 8 | 9 | public class LuminousService implements IAdblockService { 10 | @Override 11 | public String friendlyName() { 12 | return str("revanced_proxy_luminous"); 13 | } 14 | 15 | @Override 16 | public Integer maxAttempts() { 17 | return 2; 18 | } 19 | 20 | @Override 21 | public Boolean isAvailable() { 22 | return true; 23 | } 24 | 25 | @Override 26 | public Request rewriteHlsRequest(Request originalRequest) { 27 | var type = IAdblockService.isVod(originalRequest) ? "vod" : "playlist"; 28 | var url = HttpUrl.parse("https://eu.luminous.dev/" + 29 | type + 30 | "/" + 31 | IAdblockService.channelName(originalRequest) + 32 | ".m3u8" + 33 | "%3Fallow_source%3Dtrue%26allow_audio_only%3Dtrue%26fast_bread%3Dtrue" 34 | ); 35 | 36 | if (url == null) { 37 | Logger.printException(() -> "Failed to parse rewritten URL"); 38 | return null; 39 | } 40 | 41 | // Overwrite old request 42 | return new Request.Builder() 43 | .get() 44 | .url(url) 45 | .build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/api/PurpleAdblockApi.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.api; 2 | 3 | import okhttp3.ResponseBody; 4 | import retrofit2.Call; 5 | import retrofit2.http.GET; 6 | import retrofit2.http.Url; 7 | 8 | /* only used for service pings */ 9 | public interface PurpleAdblockApi { 10 | @GET /* root */ 11 | Call ping(@Url String baseUrl); 12 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/api/RetrofitClient.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.api; 2 | 3 | import retrofit2.Retrofit; 4 | 5 | public class RetrofitClient { 6 | 7 | private static RetrofitClient instance = null; 8 | private final PurpleAdblockApi purpleAdblockApi; 9 | 10 | private RetrofitClient() { 11 | Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build(); 12 | purpleAdblockApi = retrofit.create(PurpleAdblockApi.class); 13 | } 14 | 15 | public static synchronized RetrofitClient getInstance() { 16 | if (instance == null) { 17 | instance = new RetrofitClient(); 18 | } 19 | return instance; 20 | } 21 | 22 | public PurpleAdblockApi getPurpleAdblockApi() { 23 | return purpleAdblockApi; 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/patches/AudioAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.patches; 2 | 3 | import app.revanced.integrations.twitch.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class AudioAdsPatch { 7 | public static boolean shouldBlockAudioAds() { 8 | return Settings.BLOCK_AUDIO_ADS.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/patches/AutoClaimChannelPointsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.patches; 2 | 3 | import app.revanced.integrations.twitch.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class AutoClaimChannelPointsPatch { 7 | public static boolean shouldAutoClaim() { 8 | return Settings.AUTO_CLAIM_CHANNEL_POINTS.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/patches/DebugModePatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.patches; 2 | 3 | import app.revanced.integrations.twitch.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class DebugModePatch { 7 | public static boolean isDebugModeEnabled() { 8 | return Settings.TWITCH_DEBUG_MODE.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/patches/EmbeddedAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.patches; 2 | 3 | import app.revanced.integrations.twitch.api.RequestInterceptor; 4 | 5 | @SuppressWarnings("unused") 6 | public class EmbeddedAdsPatch { 7 | public static RequestInterceptor createRequestInterceptor() { 8 | return new RequestInterceptor(); 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/patches/ShowDeletedMessagesPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.patches; 2 | 3 | import static app.revanced.integrations.shared.StringRef.str; 4 | 5 | import android.graphics.Color; 6 | import android.graphics.Typeface; 7 | import android.text.SpannableStringBuilder; 8 | import android.text.Spanned; 9 | import android.text.SpannedString; 10 | import android.text.style.ForegroundColorSpan; 11 | import android.text.style.StrikethroughSpan; 12 | import android.text.style.StyleSpan; 13 | 14 | import androidx.annotation.Nullable; 15 | 16 | import app.revanced.integrations.twitch.settings.Settings; 17 | import tv.twitch.android.shared.chat.util.ClickableUsernameSpan; 18 | 19 | @SuppressWarnings("unused") 20 | public class ShowDeletedMessagesPatch { 21 | 22 | /** 23 | * Injection point. 24 | */ 25 | public static boolean shouldUseSpoiler() { 26 | return "spoiler".equals(Settings.SHOW_DELETED_MESSAGES.get()); 27 | } 28 | 29 | public static boolean shouldCrossOut() { 30 | return "cross-out".equals(Settings.SHOW_DELETED_MESSAGES.get()); 31 | } 32 | 33 | @Nullable 34 | public static Spanned reformatDeletedMessage(Spanned original) { 35 | if (!shouldCrossOut()) 36 | return null; 37 | 38 | SpannableStringBuilder ssb = new SpannableStringBuilder(original); 39 | ssb.setSpan(new StrikethroughSpan(), 0, original.length(), 0); 40 | ssb.append(" (").append(str("revanced_deleted_msg")).append(")"); 41 | ssb.setSpan(new StyleSpan(Typeface.ITALIC), original.length(), ssb.length(), 0); 42 | 43 | // Gray-out username 44 | ClickableUsernameSpan[] usernameSpans = original.getSpans(0, original.length(), ClickableUsernameSpan.class); 45 | if (usernameSpans.length > 0) { 46 | ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#ADADB8")), 0, original.getSpanEnd(usernameSpans[0]), 0); 47 | } 48 | 49 | return new SpannedString(ssb); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/patches/VideoAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.patches; 2 | 3 | import app.revanced.integrations.twitch.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class VideoAdsPatch { 7 | public static boolean shouldBlockVideoAds() { 8 | return Settings.BLOCK_VIDEO_ADS.get(); 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/settings/Settings.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.settings; 2 | 3 | import app.revanced.integrations.shared.settings.BooleanSetting; 4 | import app.revanced.integrations.shared.settings.BaseSettings; 5 | import app.revanced.integrations.shared.settings.StringSetting; 6 | 7 | import static java.lang.Boolean.FALSE; 8 | import static java.lang.Boolean.TRUE; 9 | 10 | public class Settings extends BaseSettings { 11 | /* Ads */ 12 | public static final BooleanSetting BLOCK_VIDEO_ADS = new BooleanSetting("revanced_block_video_ads", TRUE); 13 | public static final BooleanSetting BLOCK_AUDIO_ADS = new BooleanSetting("revanced_block_audio_ads", TRUE); 14 | public static final StringSetting BLOCK_EMBEDDED_ADS = new StringSetting("revanced_block_embedded_ads", "luminous"); 15 | 16 | /* Chat */ 17 | public static final StringSetting SHOW_DELETED_MESSAGES = new StringSetting("revanced_show_deleted_messages", "cross-out"); 18 | public static final BooleanSetting AUTO_CLAIM_CHANNEL_POINTS = new BooleanSetting("revanced_auto_claim_channel_points", TRUE); 19 | 20 | /* Misc */ 21 | /** 22 | * Not to be confused with {@link BaseSettings#DEBUG}. 23 | */ 24 | public static final BooleanSetting TWITCH_DEBUG_MODE = new BooleanSetting("revanced_twitch_debug_mode", FALSE, true); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/settings/preference/CustomPreferenceCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.preference.PreferenceCategory; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | public class CustomPreferenceCategory extends PreferenceCategory { 11 | public CustomPreferenceCategory(Context context, AttributeSet attrs) { 12 | super(context, attrs); 13 | } 14 | 15 | @Override 16 | protected void onBindView(View rootView) { 17 | super.onBindView(rootView); 18 | 19 | if(rootView instanceof TextView) { 20 | ((TextView) rootView).setTextColor(Color.parseColor("#8161b3")); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitch/settings/preference/ReVancedPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitch.settings.preference; 2 | 3 | import app.revanced.integrations.shared.Logger; 4 | import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment; 5 | import app.revanced.integrations.twitch.settings.Settings; 6 | 7 | /** 8 | * Preference fragment for ReVanced settings 9 | */ 10 | public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { 11 | 12 | @Override 13 | protected void initialize() { 14 | super.initialize(); 15 | 16 | // Do anything that forces this apps Settings bundle to load. 17 | if (Settings.BLOCK_VIDEO_ADS.get()) { 18 | Logger.printDebug(() -> "Block video ads enabled"); // Any statement that references the app settings. 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/json/BaseJsonHook.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.json 2 | 3 | import org.json.JSONObject 4 | 5 | abstract class BaseJsonHook : JsonHook { 6 | abstract fun apply(json: JSONObject) 7 | 8 | override fun transform(json: JSONObject) = json.apply { apply(json) } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/json/JsonHook.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.json 2 | 3 | import app.revanced.integrations.twitter.patches.hook.patch.Hook 4 | import org.json.JSONObject 5 | 6 | interface JsonHook : Hook { 7 | /** 8 | * Transform a JSONObject. 9 | * 10 | * @param json The JSONObject. 11 | */ 12 | fun transform(json: JSONObject): JSONObject 13 | 14 | override fun hook(type: JSONObject) = transform(type) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/json/JsonHookPatch.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.json 2 | 3 | import app.revanced.integrations.twitter.patches.hook.patch.dummy.DummyHook 4 | import app.revanced.integrations.twitter.utils.json.JsonUtils.parseJson 5 | import app.revanced.integrations.twitter.utils.stream.StreamUtils 6 | import org.json.JSONException 7 | import java.io.IOException 8 | import java.io.InputStream 9 | 10 | object JsonHookPatch { 11 | // Additional hooks added by corresponding patch. 12 | private val hooks = buildList { 13 | add(DummyHook) 14 | } 15 | 16 | @JvmStatic 17 | fun parseJsonHook(jsonInputStream: InputStream): InputStream { 18 | var jsonObject = try { 19 | parseJson(jsonInputStream) 20 | } catch (ignored: IOException) { 21 | return jsonInputStream // Unreachable. 22 | } catch (ignored: JSONException) { 23 | return jsonInputStream 24 | } 25 | 26 | for (hook in hooks) jsonObject = hook.hook(jsonObject) 27 | 28 | return StreamUtils.fromString(jsonObject.toString()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/patch/Hook.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.patch 2 | 3 | interface Hook { 4 | /** 5 | * Hook the given type. 6 | * @param type The type to hook 7 | */ 8 | fun hook(type: T): T 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/patch/ads/AdsHook.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.patch.ads 2 | 3 | import app.revanced.integrations.twitter.patches.hook.json.BaseJsonHook 4 | import app.revanced.integrations.twitter.patches.hook.twifucker.TwiFucker 5 | import org.json.JSONObject 6 | 7 | object AdsHook : BaseJsonHook() { 8 | /** 9 | * Strips JSONObject from promoted ads. 10 | * 11 | * @param json The JSONObject. 12 | */ 13 | override fun apply(json: JSONObject) = TwiFucker.hidePromotedAds(json) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/patch/dummy/DummyHook.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.patch.dummy 2 | 3 | import app.revanced.integrations.twitter.patches.hook.json.BaseJsonHook 4 | import app.revanced.integrations.twitter.patches.hook.json.JsonHookPatch 5 | import org.json.JSONObject 6 | 7 | /** 8 | * Dummy hook to reserve a register in [JsonHookPatch.hooks] list. 9 | */ 10 | object DummyHook : BaseJsonHook() { 11 | override fun apply(json: JSONObject) { 12 | // Do nothing. 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/patch/recommendation/RecommendedUsersHook.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.patch.recommendation 2 | 3 | import app.revanced.integrations.twitter.patches.hook.json.BaseJsonHook 4 | import app.revanced.integrations.twitter.patches.hook.twifucker.TwiFucker 5 | import org.json.JSONObject 6 | 7 | object RecommendedUsersHook : BaseJsonHook() { 8 | /** 9 | * Strips JSONObject from recommended users. 10 | * 11 | * @param json The JSONObject. 12 | */ 13 | override fun apply(json: JSONObject) = TwiFucker.hideRecommendedUsers(json) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/hook/twifucker/TwiFuckerUtils.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.hook.twifucker 2 | 3 | import org.json.JSONArray 4 | import org.json.JSONObject 5 | 6 | internal object TwiFuckerUtils { 7 | inline fun JSONArray.forEach(action: (JSONObject) -> Unit) { 8 | (0 until this.length()).forEach { i -> 9 | if (this[i] is JSONObject) { 10 | action(this[i] as JSONObject) 11 | } 12 | } 13 | } 14 | 15 | inline fun JSONArray.forEachIndexed(action: (index: Int, JSONObject) -> Unit) { 16 | (0 until this.length()).forEach { i -> 17 | if (this[i] is JSONObject) { 18 | action(i, this[i] as JSONObject) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/links/ChangeLinkSharingDomainPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.links; 2 | 3 | public final class ChangeLinkSharingDomainPatch { 4 | private static final String DOMAIN_NAME = "https://fxtwitter.com"; 5 | private static final String LINK_FORMAT = "%s/%s/status/%s"; 6 | 7 | public static String formatResourceLink(Object... formatArgs) { 8 | String username = (String) formatArgs[0]; 9 | String tweetId = (String) formatArgs[1]; 10 | return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId); 11 | } 12 | 13 | public static String formatLink(long tweetId, String username) { 14 | return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/patches/links/OpenLinksWithAppChooserPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.patches.links; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.util.Log; 6 | 7 | public final class OpenLinksWithAppChooserPatch { 8 | public static void openWithChooser(final Context context, final Intent intent) { 9 | Log.d("ReVanced", "Opening intent with chooser: " + intent); 10 | 11 | intent.setAction("android.intent.action.VIEW"); 12 | 13 | context.startActivity(Intent.createChooser(intent, null)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/utils/json/JsonUtils.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.utils.json 2 | 3 | import app.revanced.integrations.twitter.utils.stream.StreamUtils 4 | import org.json.JSONException 5 | import org.json.JSONObject 6 | import java.io.IOException 7 | import java.io.InputStream 8 | 9 | object JsonUtils { 10 | @JvmStatic 11 | @Throws(IOException::class, JSONException::class) 12 | fun parseJson(jsonInputStream: InputStream) = JSONObject(StreamUtils.toString(jsonInputStream)) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/twitter/utils/stream/StreamUtils.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.twitter.utils.stream 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.ByteArrayOutputStream 5 | import java.io.IOException 6 | import java.io.InputStream 7 | 8 | object StreamUtils { 9 | @Throws(IOException::class) 10 | fun toString(inputStream: InputStream): String { 11 | ByteArrayOutputStream().use { result -> 12 | val buffer = ByteArray(1024) 13 | var length: Int 14 | while (inputStream.read(buffer).also { length = it } != -1) { 15 | result.write(buffer, 0, length) 16 | } 17 | return result.toString() 18 | } 19 | } 20 | 21 | fun fromString(string: String): InputStream { 22 | return ByteArrayInputStream(string.toByteArray()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/ByteTrieSearch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube; 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 | ByteTrieNode(char nodeCharacterValue) { 14 | super(nodeCharacterValue); 15 | } 16 | @Override 17 | TrieNode createNode(char nodeCharacterValue) { 18 | return new ByteTrieNode(nodeCharacterValue); 19 | } 20 | @Override 21 | char getCharValue(byte[] text, int index) { 22 | return (char) text[index]; 23 | } 24 | @Override 25 | int getTextLength(byte[] text) { 26 | return text.length; 27 | } 28 | } 29 | 30 | /** 31 | * Helper method for the common usage of converting Strings to raw UTF-8 bytes. 32 | */ 33 | public static byte[][] convertStringsToBytes(String... strings) { 34 | final int length = strings.length; 35 | byte[][] replacement = new byte[length][]; 36 | for (int i = 0; i < length; i++) { 37 | replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8); 38 | } 39 | return replacement; 40 | } 41 | 42 | public ByteTrieSearch(@NonNull byte[]... patterns) { 43 | super(new ByteTrieNode(), patterns); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/Event.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube 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 | 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 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/StringTrieSearch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube; 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 | StringTrieNode(char nodeCharacterValue) { 15 | super(nodeCharacterValue); 16 | } 17 | @Override 18 | TrieNode createNode(char nodeValue) { 19 | return new StringTrieNode(nodeValue); 20 | } 21 | @Override 22 | char getCharValue(String text, int index) { 23 | return text.charAt(index); 24 | } 25 | @Override 26 | int getTextLength(String text) { 27 | return text.length(); 28 | } 29 | } 30 | 31 | public StringTrieSearch(@NonNull String... patterns) { 32 | super(new StringTrieNode(), patterns); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Color; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.Logger; 9 | import app.revanced.integrations.shared.Utils; 10 | 11 | public class ThemeHelper { 12 | @Nullable 13 | private static Integer darkThemeColor, lightThemeColor; 14 | private static int themeValue; 15 | 16 | /** 17 | * Injection point. 18 | */ 19 | @SuppressWarnings("unused") 20 | public static void setTheme(Enum value) { 21 | final int newOrdinalValue = value.ordinal(); 22 | if (themeValue != newOrdinalValue) { 23 | themeValue = newOrdinalValue; 24 | Logger.printDebug(() -> "Theme value: " + newOrdinalValue); 25 | } 26 | } 27 | 28 | public static boolean isDarkTheme() { 29 | return themeValue == 1; 30 | } 31 | 32 | public static void setActivityTheme(Activity activity) { 33 | final var theme = isDarkTheme() 34 | ? "Theme.YouTube.Settings.Dark" 35 | : "Theme.YouTube.Settings"; 36 | activity.setTheme(Utils.getResourceIdentifier(theme, "style")); 37 | } 38 | 39 | /** 40 | * Injection point. 41 | */ 42 | @SuppressWarnings("SameReturnValue") 43 | private static String darkThemeResourceName() { 44 | // Value is changed by Theme patch, if included. 45 | return "@color/yt_black3"; 46 | } 47 | 48 | /** 49 | * @return The dark theme color as specified by the Theme patch (if included), 50 | * or the dark mode background color unpatched YT uses. 51 | */ 52 | public static int getDarkThemeColor() { 53 | if (darkThemeColor == null) { 54 | darkThemeColor = getColorInt(darkThemeResourceName()); 55 | } 56 | return darkThemeColor; 57 | } 58 | 59 | /** 60 | * Injection point. 61 | */ 62 | @SuppressWarnings("SameReturnValue") 63 | private static String lightThemeResourceName() { 64 | // Value is changed by Theme patch, if included. 65 | return "@color/yt_white1"; 66 | } 67 | 68 | /** 69 | * @return The light theme color as specified by the Theme patch (if included), 70 | * or the non dark mode background color unpatched YT uses. 71 | */ 72 | public static int getLightThemeColor() { 73 | if (lightThemeColor == null) { 74 | lightThemeColor = getColorInt(lightThemeResourceName()); 75 | } 76 | return lightThemeColor; 77 | } 78 | 79 | private static int getColorInt(String colorString) { 80 | if (colorString.startsWith("#")) { 81 | return Color.parseColor(colorString); 82 | } 83 | return Utils.getResourceColor(colorString); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/AutoRepeatPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class AutoRepeatPatch { 7 | //Used by app.revanced.patches.youtube.layout.autorepeat.patch.AutoRepeatPatch 8 | public static boolean shouldAutoRepeat() { 9 | return Settings.AUTO_REPEAT.get(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.shared.PlayerType; 4 | 5 | @SuppressWarnings("unused") 6 | public class BackgroundPlaybackPatch { 7 | 8 | /** 9 | * Injection point. 10 | */ 11 | public static boolean allowBackgroundPlayback(boolean original) { 12 | if (original) return true; 13 | 14 | // Steps to verify most edge cases: 15 | // 1. Open a regular video 16 | // 2. Minimize app (PIP should appear) 17 | // 3. Reopen app 18 | // 4. Open a Short (without closing the regular video) 19 | // (try opening both Shorts in the video player suggestions AND Shorts from the home feed) 20 | // 5. Minimize the app (PIP should not appear) 21 | // 6. Reopen app 22 | // 7. Close the Short 23 | // 8. Resume playing the regular video 24 | // 9. Minimize the app (PIP should appear) 25 | 26 | if (!VideoInformation.lastVideoIdIsShort()) { 27 | return true; // Definitely is not a Short. 28 | } 29 | 30 | // Might be a Short, or might be a prior regular video on screen again after a Short was closed. 31 | // This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Short, 32 | // But there's no way around this unless an additional hook is added to definitively detect 33 | // the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern. 34 | return !PlayerType.getCurrent().isNoneHiddenOrMinimized(); 35 | } 36 | 37 | /** 38 | * Injection point. 39 | */ 40 | public static boolean overrideBackgroundPlaybackAvailable() { 41 | // This could be done entirely in the patch, 42 | // but having a unique method to search for makes manually inspecting the patched apk much easier. 43 | return true; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/BypassImageRegionRestrictionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import static app.revanced.integrations.youtube.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | import app.revanced.integrations.shared.Logger; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | @SuppressWarnings("unused") 11 | public final class BypassImageRegionRestrictionsPatch { 12 | 13 | private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BYPASS_IMAGE_REGION_RESTRICTIONS.get(); 14 | 15 | private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com"; 16 | 17 | /** 18 | * YouTube static images domain. Includes user and channel avatar images and community post images. 19 | */ 20 | private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN 21 | = Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com"); 22 | 23 | /** 24 | * Injection point. Called off the main thread and by multiple threads at the same time. 25 | * 26 | * @param originalUrl Image url for all image urls loaded. 27 | */ 28 | public static String overrideImageURL(String originalUrl) { 29 | try { 30 | if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) { 31 | String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN 32 | .matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN); 33 | 34 | if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) { 35 | Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'"); 36 | } 37 | 38 | return replacement; 39 | } 40 | } catch (Exception ex) { 41 | Logger.printException(() -> "overrideImageURL failure", ex); 42 | } 43 | 44 | return originalUrl; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/BypassURLRedirectsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.net.Uri; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | import app.revanced.integrations.shared.Logger; 7 | 8 | @SuppressWarnings("unused") 9 | public class BypassURLRedirectsPatch { 10 | private static final String YOUTUBE_REDIRECT_PATH = "/redirect"; 11 | 12 | /** 13 | * Convert the YouTube redirect URI string to the redirect query URI. 14 | * 15 | * @param uri The YouTube redirect URI string. 16 | * @return The redirect query URI. 17 | */ 18 | public static Uri parseRedirectUri(String uri) { 19 | final var parsed = Uri.parse(uri); 20 | 21 | if (Settings.BYPASS_URL_REDIRECTS.get() && parsed.getPath().equals(YOUTUBE_REDIRECT_PATH)) { 22 | var query = Uri.parse(Uri.decode(parsed.getQueryParameter("q"))); 23 | 24 | Logger.printDebug(() -> "Bypassing YouTube redirect URI: " + query); 25 | 26 | return query; 27 | } 28 | 29 | return parsed; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/CopyVideoUrlPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import static app.revanced.integrations.shared.StringRef.str; 4 | 5 | import android.os.Build; 6 | 7 | import app.revanced.integrations.shared.Logger; 8 | import app.revanced.integrations.shared.Utils; 9 | 10 | public class CopyVideoUrlPatch { 11 | 12 | public static void copyUrl(boolean withTimestamp) { 13 | try { 14 | StringBuilder builder = new StringBuilder("https://youtu.be/"); 15 | builder.append(VideoInformation.getVideoId()); 16 | final long currentVideoTimeInSeconds = VideoInformation.getVideoTime() / 1000; 17 | if (withTimestamp && currentVideoTimeInSeconds > 0) { 18 | final long hour = currentVideoTimeInSeconds / (60 * 60); 19 | final long minute = (currentVideoTimeInSeconds / 60) % 60; 20 | final long second = currentVideoTimeInSeconds % 60; 21 | builder.append("?t="); 22 | if (hour > 0) { 23 | builder.append(hour).append("h"); 24 | } 25 | if (minute > 0) { 26 | builder.append(minute).append("m"); 27 | } 28 | if (second > 0) { 29 | builder.append(second).append("s"); 30 | } 31 | } 32 | 33 | Utils.setClipboard(builder.toString()); 34 | // Do not show a toast if using Android 13+ as it shows it's own toast. 35 | // But if the user copied with a timestamp then show a toast. 36 | // Unfortunately this will show 2 toasts on Android 13+, but no way around this. 37 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) { 38 | Utils.showToastShort(withTimestamp && currentVideoTimeInSeconds > 0 39 | ? str("revanced_share_copy_url_timestamp_success") 40 | : str("revanced_share_copy_url_success")); 41 | } 42 | } catch (Exception e) { 43 | Logger.printException(() -> "Failed to generate video url", e); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/CustomPlayerOverlayOpacityPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import static app.revanced.integrations.shared.StringRef.str; 4 | 5 | import android.widget.ImageView; 6 | 7 | import app.revanced.integrations.shared.Utils; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | @SuppressWarnings("unused") 11 | public class CustomPlayerOverlayOpacityPatch { 12 | 13 | private static final int PLAYER_OVERLAY_OPACITY_LEVEL; 14 | 15 | static { 16 | int opacity = Settings.PLAYER_OVERLAY_OPACITY.get(); 17 | 18 | if (opacity < 0 || opacity > 100) { 19 | Utils.showToastLong(str("revanced_player_overlay_opacity_invalid_toast")); 20 | Settings.PLAYER_OVERLAY_OPACITY.resetToDefault(); 21 | opacity = Settings.PLAYER_OVERLAY_OPACITY.defaultValue; 22 | } 23 | 24 | PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100; 25 | } 26 | 27 | /** 28 | * Injection point. 29 | */ 30 | public static void changeOpacity(ImageView imageView) { 31 | imageView.setImageAlpha(PLAYER_OVERLAY_OPACITY_LEVEL); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisableAutoCaptionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | import app.revanced.integrations.youtube.shared.PlayerType; 5 | 6 | @SuppressWarnings("unused") 7 | public class DisableAutoCaptionsPatch { 8 | 9 | /** 10 | * Used by injected code. Do not delete. 11 | */ 12 | public static boolean captionsButtonDisabled; 13 | 14 | public static boolean autoCaptionsEnabled() { 15 | return Settings.AUTO_CAPTIONS.get() 16 | // Do not use auto captions for Shorts. 17 | && !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisableFullscreenAmbientModePatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | /** @noinspection unused*/ 6 | public final class DisableFullscreenAmbientModePatch { 7 | public static boolean enableFullScreenAmbientMode() { 8 | return !Settings.DISABLE_FULLSCREEN_AMBIENT_MODE.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisablePlayerPopupPanelsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class DisablePlayerPopupPanelsPatch { 7 | //Used by app.revanced.patches.youtube.layout.playerpopuppanels.patch.PlayerPopupPanelsPatch 8 | public static boolean disablePlayerPopupPanels() { 9 | return Settings.PLAYER_POPUP_PANELS.get(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisablePreciseSeekingGesturePatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class DisablePreciseSeekingGesturePatch { 7 | public static boolean isGestureDisabled() { 8 | return Settings.DISABLE_PRECISE_SEEKING_GESTURE.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisableResumingStartupShortsPlayerPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | /** @noinspection unused*/ 6 | public class DisableResumingStartupShortsPlayerPatch { 7 | 8 | /** 9 | * Injection point. 10 | */ 11 | public static boolean disableResumingStartupShortsPlayer() { 12 | return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisableRollingNumberAnimationsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class DisableRollingNumberAnimationsPatch { 7 | /** 8 | * Injection point. 9 | */ 10 | public static boolean disableRollingNumberAnimations() { 11 | return Settings.DISABLE_ROLLING_NUMBER_ANIMATIONS.get(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/DisableSuggestedVideoEndScreenPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.widget.ImageView; 5 | 6 | import app.revanced.integrations.youtube.settings.Settings; 7 | 8 | /** @noinspection unused*/ 9 | public final class DisableSuggestedVideoEndScreenPatch { 10 | @SuppressLint("StaticFieldLeak") 11 | private static ImageView lastView; 12 | 13 | public static void closeEndScreen(final ImageView imageView) { 14 | if (!Settings.DISABLE_SUGGESTED_VIDEO_END_SCREEN.get()) return; 15 | 16 | // Prevent adding the listener multiple times. 17 | if (lastView == imageView) return; 18 | lastView = imageView; 19 | 20 | imageView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { 21 | if (imageView.isShown()) imageView.callOnClick(); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/FixBackToExitGesturePatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.app.Activity; 4 | 5 | import app.revanced.integrations.shared.Logger; 6 | 7 | @SuppressWarnings("unused") 8 | public class FixBackToExitGesturePatch { 9 | /** 10 | * State whether the scroll position reaches the top. 11 | */ 12 | public static boolean isTopView = false; 13 | 14 | /** 15 | * Handle the event after clicking the back button. 16 | * 17 | * @param activity The activity, the app is launched with to finish. 18 | */ 19 | public static void onBackPressed(Activity activity) { 20 | if (!isTopView) return; 21 | 22 | Logger.printDebug(() -> "Activity is closed"); 23 | 24 | activity.finish(); 25 | } 26 | 27 | /** 28 | * Handle the event when the homepage list of views is being scrolled. 29 | */ 30 | public static void onScrollingViews() { 31 | Logger.printDebug(() -> "Views are scrolling"); 32 | 33 | isTopView = false; 34 | } 35 | 36 | /** 37 | * Handle the event when the homepage list of views reached the top. 38 | */ 39 | public static void onTopView() { 40 | Logger.printDebug(() -> "Scrolling reached the top"); 41 | 42 | isTopView = true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/FullscreenPanelsRemoverPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class FullscreenPanelsRemoverPatch { 9 | public static int getFullscreenPanelsVisibility() { 10 | return Settings.HIDE_FULLSCREEN_PANELS.get() ? View.GONE : View.VISIBLE; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HDRAutoBrightnessPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.WindowManager; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | import app.revanced.integrations.youtube.swipecontrols.SwipeControlsHostActivity; 7 | 8 | /** 9 | * Patch class for 'hdr-auto-brightness' patch. 10 | * 11 | * Edit: This patch no longer does anything, as YT already uses BRIGHTNESS_OVERRIDE_NONE 12 | * as the default brightness level. The hooked code was also removed from YT 19.09+ as well. 13 | */ 14 | @Deprecated 15 | @SuppressWarnings("unused") 16 | public class HDRAutoBrightnessPatch { 17 | /** 18 | * get brightness override for HDR brightness 19 | * 20 | * @param original brightness youtube would normally set 21 | * @return brightness to set on HRD video 22 | */ 23 | public static float getHDRBrightness(float original) { 24 | // do nothing if disabled 25 | if (!Settings.HDR_AUTO_BRIGHTNESS.get()) { 26 | return original; 27 | } 28 | 29 | // override with brightness set by swipe-controls 30 | // only when swipe-controls is active and has overridden the brightness 31 | final SwipeControlsHostActivity swipeControlsHost = SwipeControlsHostActivity.getCurrentHost().get(); 32 | if (swipeControlsHost != null 33 | && swipeControlsHost.getScreen() != null 34 | && swipeControlsHost.getConfig().getEnableBrightnessControl() 35 | && !swipeControlsHost.getScreen().isDefaultBrightness()) { 36 | return swipeControlsHost.getScreen().getRawScreenBrightness(); 37 | } 38 | 39 | // otherwise, set the brightness to auto 40 | return WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideAlbumCardsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | import app.revanced.integrations.shared.Utils; 7 | 8 | @SuppressWarnings("unused") 9 | public class HideAlbumCardsPatch { 10 | public static void hideAlbumCard(View view) { 11 | if (!Settings.HIDE_ALBUM_CARDS.get()) return; 12 | Utils.hideViewByLayoutParams(view); 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideAutoplayButtonPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class HideAutoplayButtonPatch { 7 | 8 | private static final boolean HIDE_AUTOPLAY_BUTTON_ENABLED = Settings.HIDE_AUTOPLAY_BUTTON.get(); 9 | 10 | /** 11 | * Injection point. 12 | */ 13 | public static boolean hideAutoPlayButton() { 14 | return HIDE_AUTOPLAY_BUTTON_ENABLED; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideCaptionsButtonPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.widget.ImageView; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class HideCaptionsButtonPatch { 9 | //Used by app.revanced.patches.youtube.layout.hidecaptionsbutton.patch.HideCaptionsButtonPatch 10 | public static void hideCaptionsButton(ImageView imageView) { 11 | imageView.setVisibility(Settings.HIDE_CAPTIONS_BUTTON.get() ? ImageView.GONE : ImageView.VISIBLE); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideCastButtonPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class HideCastButtonPatch { 9 | 10 | // Used by app.revanced.patches.youtube.layout.castbutton.patch.HideCastButonPatch 11 | public static int getCastButtonOverrideV2(int original) { 12 | return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideCrowdfundingBoxPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | import app.revanced.integrations.shared.Utils; 7 | 8 | @SuppressWarnings("unused") 9 | public class HideCrowdfundingBoxPatch { 10 | //Used by app.revanced.patches.youtube.layout.hidecrowdfundingbox.patch.HideCrowdfundingBoxPatch 11 | public static void hideCrowdfundingBox(View view) { 12 | if (!Settings.HIDE_CROWDFUNDING_BOX.get()) return; 13 | Utils.hideViewByLayoutParams(view); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideEmailAddressPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | /** 6 | * Patch is obsolete and will be deleted in a future release 7 | */ 8 | @SuppressWarnings("unused") 9 | @Deprecated() 10 | public class HideEmailAddressPatch { 11 | //Used by app.revanced.patches.youtube.layout.personalinformation.patch.HideEmailAddressPatch 12 | public static int hideEmailAddress(int originalValue) { 13 | if (Settings.HIDE_EMAIL_ADDRESS.get()) 14 | return 8; 15 | return originalValue; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideEndscreenCardsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class HideEndscreenCardsPatch { 9 | //Used by app.revanced.patches.youtube.layout.hideendscreencards.bytecode.patch.HideEndscreenCardsPatch 10 | public static void hideEndscreen(View view) { 11 | if (!Settings.HIDE_ENDSCREEN_CARDS.get()) return; 12 | view.setVisibility(View.GONE); 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideFilterBarPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | import app.revanced.integrations.shared.Utils; 7 | 8 | @SuppressWarnings("unused") 9 | public final class HideFilterBarPatch { 10 | public static int hideInFeed(final int height) { 11 | if (Settings.HIDE_FILTER_BAR_FEED_IN_FEED.get()) return 0; 12 | 13 | return height; 14 | } 15 | 16 | public static void hideInRelatedVideos(final View chipView) { 17 | if (!Settings.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS.get()) return; 18 | 19 | Utils.hideViewByLayoutParams(chipView); 20 | } 21 | 22 | public static int hideInSearch(final int height) { 23 | if (Settings.HIDE_FILTER_BAR_FEED_IN_SEARCH.get()) return 0; 24 | 25 | return height; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideFloatingMicrophoneButtonPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class HideFloatingMicrophoneButtonPatch { 7 | public static boolean hideFloatingMicrophoneButton(final boolean original) { 8 | return Settings.HIDE_FLOATING_MICROPHONE_BUTTON.get() || original; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideGetPremiumPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class HideGetPremiumPatch { 7 | /** 8 | * Injection point. 9 | */ 10 | public static boolean hideGetPremiumView() { 11 | return Settings.HIDE_GET_PREMIUM.get(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideInfoCardsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.youtube.settings.Settings; 6 | 7 | @SuppressWarnings("unused") 8 | public class HideInfoCardsPatch { 9 | public static void hideInfoCardsIncognito(View view) { 10 | if (!Settings.HIDE_INFO_CARDS.get()) return; 11 | view.setVisibility(View.GONE); 12 | } 13 | 14 | public static boolean hideInfoCardsMethodCall() { 15 | return Settings.HIDE_INFO_CARDS.get(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | 5 | import app.revanced.integrations.shared.Logger; 6 | import app.revanced.integrations.shared.Utils; 7 | import app.revanced.integrations.youtube.settings.Settings; 8 | 9 | @SuppressWarnings("unused") 10 | public final class HidePlayerButtonsPatch { 11 | 12 | private static final boolean HIDE_PLAYER_BUTTONS_ENABLED = Settings.HIDE_PLAYER_BUTTONS.get(); 13 | 14 | private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = 15 | Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id"); 16 | 17 | private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = 18 | Utils.getResourceIdentifier("player_control_next_button_touch_area", "id"); 19 | 20 | /** 21 | * Injection point. 22 | */ 23 | public static void hidePreviousNextButtons(View parentView) { 24 | if (!HIDE_PLAYER_BUTTONS_ENABLED) { 25 | return; 26 | } 27 | 28 | // Must use a deferred call to main thread to hide the button. 29 | // Otherwise the layout crashes if set to hidden now. 30 | Utils.runOnMainThread(() -> { 31 | hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID); 32 | hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID); 33 | }); 34 | } 35 | 36 | private static void hideView(View parentView, int resourceId) { 37 | View nextPreviousButton = parentView.findViewById(resourceId); 38 | 39 | if (nextPreviousButton == null) { 40 | Logger.printException(() -> "Could not find player previous/next button"); 41 | return; 42 | } 43 | 44 | Logger.printDebug(() -> "Hiding previous/next button"); 45 | Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideSeekbarPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class HideSeekbarPatch { 7 | public static boolean hideSeekbar() { 8 | return Settings.HIDE_SEEKBAR.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/HideTimestampPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class HideTimestampPatch { 7 | public static boolean hideTimestamp() { 8 | return Settings.HIDE_TIMESTAMP.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/NavigationButtonsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import static app.revanced.integrations.shared.Utils.hideViewUnderCondition; 4 | import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; 5 | 6 | import android.view.View; 7 | 8 | import java.util.EnumMap; 9 | import java.util.Map; 10 | 11 | import android.widget.TextView; 12 | import app.revanced.integrations.youtube.settings.Settings; 13 | 14 | @SuppressWarnings("unused") 15 | public final class NavigationButtonsPatch { 16 | 17 | private static final Map shouldHideMap = new EnumMap<>(NavigationButton.class) { 18 | { 19 | put(NavigationButton.HOME, Settings.HIDE_HOME_BUTTON.get()); 20 | put(NavigationButton.CREATE, Settings.HIDE_CREATE_BUTTON.get()); 21 | put(NavigationButton.SHORTS, Settings.HIDE_SHORTS_BUTTON.get()); 22 | put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_SUBSCRIPTIONS_BUTTON.get()); 23 | } 24 | }; 25 | 26 | private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON 27 | = Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(); 28 | 29 | /** 30 | * Injection point. 31 | */ 32 | public static boolean switchCreateWithNotificationButton() { 33 | return SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON; 34 | } 35 | 36 | /** 37 | * Injection point. 38 | */ 39 | public static void navigationTabCreated(NavigationButton button, View tabView) { 40 | if (Boolean.TRUE.equals(shouldHideMap.get(button))) { 41 | tabView.setVisibility(View.GONE); 42 | } 43 | } 44 | 45 | /** 46 | * Injection point. 47 | */ 48 | public static void hideNavigationButtonLabels(TextView navigationLabelsView) { 49 | hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/OpenLinksExternallyPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class OpenLinksExternallyPatch { 7 | /** 8 | * Return the intent to open links with. If empty, the link will be opened with the default browser. 9 | * 10 | * @param originalIntent The original intent to open links with. 11 | * @return The intent to open links with. Empty means the link will be opened with the default browser. 12 | */ 13 | public static String getIntent(String originalIntent) { 14 | if (Settings.EXTERNAL_BROWSER.get()) return ""; 15 | 16 | return originalIntent; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/PlayerControlsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.View; 4 | import android.view.ViewTreeObserver; 5 | import android.widget.ImageView; 6 | 7 | import app.revanced.integrations.shared.Logger; 8 | 9 | @SuppressWarnings("unused") 10 | public class PlayerControlsPatch { 11 | /** 12 | * Injection point. 13 | */ 14 | public static void setFullscreenCloseButton(ImageView imageButton) { 15 | // Add a global listener, since the protected method 16 | // View#onVisibilityChanged() does not have any call backs. 17 | imageButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 18 | int lastVisibility = View.VISIBLE; 19 | 20 | @Override 21 | public void onGlobalLayout() { 22 | try { 23 | final int visibility = imageButton.getVisibility(); 24 | if (lastVisibility != visibility) { 25 | lastVisibility = visibility; 26 | 27 | Logger.printDebug(() -> "fullscreen button visibility: " 28 | + (visibility == View.VISIBLE ? "VISIBLE" : 29 | visibility == View.GONE ? "GONE" : "INVISIBLE")); 30 | 31 | fullscreenButtonVisibilityChanged(visibility == View.VISIBLE); 32 | } 33 | } catch (Exception ex) { 34 | Logger.printDebug(() -> "OnGlobalLayoutListener failure", ex); 35 | } 36 | } 37 | }); 38 | } 39 | 40 | // noinspection EmptyMethod 41 | public static void fullscreenButtonVisibilityChanged(boolean isVisible) { 42 | // Code added during patching. 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.view.ViewGroup; 4 | 5 | import app.revanced.integrations.youtube.shared.PlayerOverlays; 6 | 7 | @SuppressWarnings("unused") 8 | public class PlayerOverlaysHookPatch { 9 | /** 10 | * Injection point. 11 | */ 12 | public static void playerOverlayInflated(ViewGroup group) { 13 | PlayerOverlays.attach(group); 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/PlayerTypeHookPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.youtube.shared.PlayerType; 6 | import app.revanced.integrations.youtube.shared.VideoState; 7 | 8 | @SuppressWarnings("unused") 9 | public class PlayerTypeHookPatch { 10 | /** 11 | * Injection point. 12 | */ 13 | public static void setPlayerType(@Nullable Enum youTubePlayerType) { 14 | if (youTubePlayerType == null) return; 15 | 16 | PlayerType.setFromString(youTubePlayerType.name()); 17 | } 18 | 19 | /** 20 | * Injection point. 21 | */ 22 | public static void setVideoState(@Nullable Enum youTubeVideoState) { 23 | if (youTubeVideoState == null) return; 24 | 25 | VideoState.setFromString(youTubeVideoState.name()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/RemoveTrackingQueryParameterPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class RemoveTrackingQueryParameterPatch { 7 | private static final String NEW_TRACKING_PARAMETER_REGEX = ".si=.+"; 8 | private static final String OLD_TRACKING_PARAMETER_REGEX = ".feature=.+"; 9 | 10 | public static String sanitize(String url) { 11 | if (!Settings.REMOVE_TRACKING_QUERY_PARAMETER.get()) return url; 12 | 13 | return url 14 | .replaceAll(NEW_TRACKING_PARAMETER_REGEX, "") 15 | .replaceAll(OLD_TRACKING_PARAMETER_REGEX, ""); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/RemoveViewerDiscretionDialogPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import android.app.AlertDialog; 4 | import app.revanced.integrations.youtube.settings.Settings; 5 | 6 | /** @noinspection unused*/ 7 | public class RemoveViewerDiscretionDialogPatch { 8 | public static void confirmDialog(AlertDialog dialog) { 9 | if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) { 10 | // Since the patch replaces the AlertDialog#show() method, we need to call the original method here. 11 | dialog.show(); 12 | return; 13 | } 14 | 15 | final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); 16 | button.setSoundEffectsEnabled(false); 17 | button.performClick(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/RestoreOldSeekbarThumbnailsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class RestoreOldSeekbarThumbnailsPatch { 7 | public static boolean useFullscreenSeekbarThumbnails() { 8 | return !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/SeekbarTappingPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class SeekbarTappingPatch { 7 | public static boolean seekbarTappingEnabled() { 8 | return Settings.SEEKBAR_TAPPING.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class SlideToSeekPatch { 7 | private static final Boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get(); 8 | 9 | public static boolean isSlideToSeekDisabled(boolean isDisabled) { 10 | if (!isDisabled) return isDisabled; 11 | 12 | return SLIDE_TO_SEEK_DISABLED; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/TabletLayoutPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class TabletLayoutPatch { 7 | 8 | private static final boolean TABLET_LAYOUT_ENABLED = Settings.TABLET_LAYOUT.get(); 9 | 10 | /** 11 | * Injection point. 12 | */ 13 | public static boolean getTabletLayoutEnabled() { 14 | return TABLET_LAYOUT_ENABLED; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.shared.Utils; 4 | 5 | public class VersionCheckPatch { 6 | public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; 7 | public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; 8 | public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; 9 | public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/VideoAdsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class VideoAdsPatch { 7 | 8 | // Used by app.revanced.patches.youtube.ad.general.video.patch.VideoAdsPatch 9 | // depends on Whitelist patch (still needs to be written) 10 | public static boolean shouldShowAds() { 11 | return !Settings.HIDE_VIDEO_ADS.get(); // TODO && Whitelist.shouldShowAds(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/WideSearchbarPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class WideSearchbarPatch { 7 | 8 | public static boolean enableWideSearchbar(boolean original) { 9 | return Settings.WIDE_SEARCHBAR.get() || original; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/ZoomHapticsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class ZoomHapticsPatch { 7 | public static boolean shouldVibrate() { 8 | return !Settings.DISABLE_ZOOM_HAPTICS.get(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/announcements/requests/AnnouncementsRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.announcements.requests; 2 | 3 | import app.revanced.integrations.youtube.requests.Requester; 4 | import app.revanced.integrations.youtube.requests.Route; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | 9 | import static app.revanced.integrations.youtube.requests.Route.Method.GET; 10 | 11 | public class AnnouncementsRoutes { 12 | private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v2"; 13 | 14 | /** 15 | * 'language' parameter is IETF format (for USA it would be 'en-us'). 16 | */ 17 | public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?language={language}"); 18 | 19 | private AnnouncementsRoutes() { 20 | } 21 | 22 | public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException { 23 | return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params); 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.RequiresApi; 7 | 8 | import java.util.*; 9 | import java.util.function.Consumer; 10 | 11 | import app.revanced.integrations.youtube.ByteTrieSearch; 12 | import app.revanced.integrations.youtube.StringTrieSearch; 13 | import app.revanced.integrations.youtube.TrieSearch; 14 | 15 | abstract class FilterGroupList> implements Iterable { 16 | 17 | private final List filterGroups = new ArrayList<>(); 18 | private final TrieSearch search = createSearchGraph(); 19 | 20 | @SafeVarargs 21 | protected final void addAll(final T... groups) { 22 | filterGroups.addAll(Arrays.asList(groups)); 23 | 24 | for (T group : groups) { 25 | if (!group.includeInSearch()) { 26 | continue; 27 | } 28 | for (V pattern : group.filters) { 29 | search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { 30 | if (group.isEnabled()) { 31 | FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; 32 | result.setValues(group.setting, matchedStartIndex, matchedLength); 33 | return true; 34 | } 35 | return false; 36 | }); 37 | } 38 | } 39 | } 40 | 41 | @NonNull 42 | @Override 43 | public Iterator iterator() { 44 | return filterGroups.iterator(); 45 | } 46 | 47 | @RequiresApi(api = Build.VERSION_CODES.N) 48 | @Override 49 | public void forEach(@NonNull Consumer action) { 50 | filterGroups.forEach(action); 51 | } 52 | 53 | @RequiresApi(api = Build.VERSION_CODES.N) 54 | @NonNull 55 | @Override 56 | public Spliterator spliterator() { 57 | return filterGroups.spliterator(); 58 | } 59 | 60 | protected FilterGroup.FilterGroupResult check(V stack) { 61 | FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); 62 | search.matches(stack, result); 63 | return result; 64 | 65 | } 66 | 67 | protected abstract TrieSearch createSearchGraph(); 68 | } 69 | 70 | final class StringFilterGroupList extends FilterGroupList { 71 | protected StringTrieSearch createSearchGraph() { 72 | return new StringTrieSearch(); 73 | } 74 | } 75 | 76 | /** 77 | * If searching for a single byte pattern, then it is slightly better to use 78 | * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster 79 | * than a prefix tree to search for only 1 pattern. 80 | */ 81 | final class ByteArrayFilterGroupList extends FilterGroupList { 82 | protected ByteTrieSearch createSearchGraph() { 83 | return new ByteTrieSearch(); 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/HideInfoCardsFilterPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public final class HideInfoCardsFilterPatch extends Filter { 7 | 8 | public HideInfoCardsFilterPatch() { 9 | addIdentifierCallbacks( 10 | new StringFilterGroup( 11 | Settings.HIDE_INFO_CARDS, 12 | "info_card_teaser_overlay.eml" 13 | ) 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; 6 | 7 | /** 8 | * Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}. 9 | */ 10 | public final class PlaybackSpeedMenuFilterPatch extends Filter { 11 | // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. 12 | public static volatile boolean isPlaybackSpeedMenuVisible; 13 | 14 | public PlaybackSpeedMenuFilterPatch() { 15 | addPathCallbacks(new StringFilterGroup( 16 | null, 17 | "playback_speed_sheet_content.eml-js" 18 | )); 19 | } 20 | 21 | @Override 22 | boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, 23 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 24 | isPlaybackSpeedMenuVisible = true; 25 | 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/components/VideoQualityMenuFilterPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.components; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import app.revanced.integrations.youtube.patches.playback.quality.RestoreOldVideoQualityMenuPatch; 6 | import app.revanced.integrations.youtube.settings.Settings; 7 | 8 | /** 9 | * Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}. 10 | */ 11 | public final class VideoQualityMenuFilterPatch extends Filter { 12 | // Must be volatile or synchronized, as litho filtering runs off main thread 13 | // and this field is then access from the main thread. 14 | public static volatile boolean isVideoQualityMenuVisible; 15 | 16 | public VideoQualityMenuFilterPatch() { 17 | addPathCallbacks(new StringFilterGroup( 18 | Settings.RESTORE_OLD_VIDEO_QUALITY_MENU, 19 | "quick_quality_sheet_content.eml-js" 20 | )); 21 | } 22 | 23 | @Override 24 | boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, 25 | StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { 26 | isVideoQualityMenuVisible = true; 27 | 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.playback.speed; 2 | 3 | import static app.revanced.integrations.shared.StringRef.str; 4 | 5 | import app.revanced.integrations.youtube.patches.VideoInformation; 6 | import app.revanced.integrations.youtube.settings.Settings; 7 | import app.revanced.integrations.shared.Logger; 8 | import app.revanced.integrations.shared.Utils; 9 | 10 | @SuppressWarnings("unused") 11 | public final class RememberPlaybackSpeedPatch { 12 | 13 | /** 14 | * Injection point. 15 | */ 16 | public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) { 17 | Logger.printDebug(() -> "newVideoStarted"); 18 | VideoInformation.overridePlaybackSpeed(Settings.PLAYBACK_SPEED_DEFAULT.get()); 19 | } 20 | 21 | /** 22 | * Injection point. 23 | * Called when user selects a playback speed. 24 | * 25 | * @param playbackSpeed The playback speed the user selected 26 | */ 27 | public static void userSelectedPlaybackSpeed(float playbackSpeed) { 28 | if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) { 29 | Settings.PLAYBACK_SPEED_DEFAULT.save(playbackSpeed); 30 | Utils.showToastLong(str("revanced_remember_playback_speed_toast", (playbackSpeed + "x"))); 31 | } 32 | } 33 | 34 | /** 35 | * Injection point. 36 | * Overrides the video speed. Called after video loads, and immediately after user selects a different playback speed 37 | */ 38 | public static float getPlaybackSpeedOverride() { 39 | return VideoInformation.getPlaybackSpeed(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spoof/ClientType.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spoof; 2 | 3 | import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1; 4 | import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9; 5 | 6 | import android.os.Build; 7 | 8 | import androidx.annotation.Nullable; 9 | 10 | public enum ClientType { 11 | // https://dumps.tadiphone.dev/dumps/oculus/eureka 12 | IOS(5, 13 | // iPhone 15 supports AV1 hardware decoding. 14 | // Only use if this Android device also has hardware decoding. 15 | allowAV1() 16 | ? "iPhone16,2" // 15 Pro Max 17 | : "iPhone11,4", // XS Max 18 | // iOS 14+ forces VP9. 19 | allowVP9() 20 | ? "17.5.1.21F90" 21 | : "13.7.17H35", 22 | allowVP9() 23 | ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)" 24 | : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)", 25 | null, 26 | // Version number should be a valid iOS release. 27 | // https://www.ipa4fun.com/history/185230 28 | "19.10.7" 29 | ), 30 | ANDROID_VR(28, 31 | "Quest 3", 32 | "12", 33 | "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", 34 | "32", // Android 12.1 35 | "1.56.21" 36 | ); 37 | 38 | /** 39 | * YouTube 40 | * client type 41 | */ 42 | public final int id; 43 | 44 | /** 45 | * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) 46 | */ 47 | public final String model; 48 | 49 | /** 50 | * Device OS version. 51 | */ 52 | public final String osVersion; 53 | 54 | /** 55 | * Player user-agent. 56 | */ 57 | public final String userAgent; 58 | 59 | /** 60 | * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) 61 | * Field is null if not applicable. 62 | */ 63 | @Nullable 64 | public final String androidSdkVersion; 65 | 66 | /** 67 | * App version. 68 | */ 69 | public final String appVersion; 70 | 71 | ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) { 72 | this.id = id; 73 | this.model = model; 74 | this.osVersion = osVersion; 75 | this.userAgent = userAgent; 76 | this.androidSdkVersion = androidSdkVersion; 77 | this.appVersion = appVersion; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spoof/DeviceHardwareSupport.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spoof; 2 | 3 | import android.media.MediaCodecInfo; 4 | import android.media.MediaCodecList; 5 | import android.os.Build; 6 | 7 | import app.revanced.integrations.shared.Logger; 8 | import app.revanced.integrations.youtube.settings.Settings; 9 | 10 | public class DeviceHardwareSupport { 11 | public static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; 12 | public static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; 13 | 14 | static { 15 | boolean vp9found = false; 16 | boolean av1found = false; 17 | MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); 18 | final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; 19 | 20 | for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { 21 | final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater 22 | ? codecInfo.isHardwareAccelerated() 23 | : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. 24 | if (isHardwareAccelerated && !codecInfo.isEncoder()) { 25 | for (String type : codecInfo.getSupportedTypes()) { 26 | if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { 27 | vp9found = true; 28 | } else if (type.equalsIgnoreCase("video/av01")) { 29 | av1found = true; 30 | } 31 | } 32 | } 33 | } 34 | 35 | DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; 36 | DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; 37 | 38 | Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 39 | ? "Device supports AV1 hardware decoding\n" 40 | : "Device does not support AV1 hardware decoding\n" 41 | + (DEVICE_HAS_HARDWARE_DECODING_VP9 42 | ? "Device supports VP9 hardware decoding" 43 | : "Device does not support VP9 hardware decoding")); 44 | } 45 | 46 | public static boolean allowVP9() { 47 | return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get(); 48 | } 49 | 50 | public static boolean allowAV1() { 51 | return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofAppVersionPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spoof; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | 5 | @SuppressWarnings("unused") 6 | public class SpoofAppVersionPatch { 7 | 8 | private static final boolean SPOOF_APP_VERSION_ENABLED = Settings.SPOOF_APP_VERSION.get(); 9 | private static final String SPOOF_APP_VERSION_TARGET = Settings.SPOOF_APP_VERSION_TARGET.get(); 10 | 11 | /** 12 | * Injection point 13 | */ 14 | public static String getYouTubeVersionOverride(String version) { 15 | if (SPOOF_APP_VERSION_ENABLED) return SPOOF_APP_VERSION_TARGET; 16 | return version; 17 | } 18 | 19 | public static boolean isSpoofingToLessThan(String version) { 20 | return SPOOF_APP_VERSION_ENABLED && SPOOF_APP_VERSION_TARGET.compareTo(version) < 0; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spoof/SpoofDeviceDimensionsPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spoof; 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 | 10 | public static int getMinHeightOrWidth(int minHeightOrWidth) { 11 | return SPOOF ? 64 : minHeightOrWidth; 12 | } 13 | 14 | public static int getMaxHeightOrWidth(int maxHeightOrWidth) { 15 | return SPOOF ? 4096 : maxHeightOrWidth; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.spoof.requests; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | 9 | import app.revanced.integrations.shared.Logger; 10 | import app.revanced.integrations.youtube.patches.spoof.ClientType; 11 | import app.revanced.integrations.youtube.requests.Requester; 12 | import app.revanced.integrations.youtube.requests.Route; 13 | 14 | final class PlayerRoutes { 15 | private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; 16 | 17 | static final Route.CompiledRoute GET_STREAMING_DATA = new Route( 18 | Route.Method.POST, 19 | "player" + 20 | "?fields=streamingData" + 21 | "&alt=proto" 22 | ).compile(); 23 | 24 | /** 25 | * TCP connection and HTTP read timeout 26 | */ 27 | private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. 28 | 29 | private PlayerRoutes() { 30 | } 31 | 32 | static String createInnertubeBody(ClientType clientType) { 33 | JSONObject innerTubeBody = new JSONObject(); 34 | 35 | try { 36 | JSONObject context = new JSONObject(); 37 | 38 | JSONObject client = new JSONObject(); 39 | client.put("clientName", clientType.name()); 40 | client.put("clientVersion", clientType.appVersion); 41 | client.put("deviceModel", clientType.model); 42 | client.put("osVersion", clientType.osVersion); 43 | if (clientType.androidSdkVersion != null) { 44 | client.put("androidSdkVersion", clientType.androidSdkVersion); 45 | } 46 | 47 | context.put("client", client); 48 | 49 | innerTubeBody.put("context", context); 50 | innerTubeBody.put("contentCheckOk", true); 51 | innerTubeBody.put("racyCheckOk", true); 52 | innerTubeBody.put("videoId", "%s"); 53 | } catch (JSONException e) { 54 | Logger.printException(() -> "Failed to create innerTubeBody", e); 55 | } 56 | 57 | return innerTubeBody.toString(); 58 | } 59 | 60 | /** @noinspection SameParameterValue*/ 61 | static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { 62 | var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); 63 | 64 | connection.setRequestProperty("Content-Type", "application/json"); 65 | connection.setRequestProperty("User-Agent", clientType.userAgent); 66 | 67 | connection.setUseCaches(false); 68 | connection.setDoOutput(true); 69 | 70 | connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); 71 | connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); 72 | return connection; 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/theme/ProgressBarDrawable.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.theme; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.ColorFilter; 5 | import android.graphics.Paint; 6 | import android.graphics.PixelFormat; 7 | import android.graphics.drawable.Drawable; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | 12 | import app.revanced.integrations.youtube.patches.HideSeekbarPatch; 13 | import app.revanced.integrations.youtube.settings.Settings; 14 | 15 | /** 16 | * Used by {@link SeekbarColorPatch} change the color of the seekbar. 17 | * and {@link HideSeekbarPatch} to hide the seekbar of the feed and watch history. 18 | */ 19 | @SuppressWarnings("unused") 20 | public class ProgressBarDrawable extends Drawable { 21 | 22 | private final Paint paint = new Paint(); 23 | 24 | @Override 25 | public void draw(@NonNull Canvas canvas) { 26 | if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { 27 | return; 28 | } 29 | paint.setColor(SeekbarColorPatch.getSeekbarColor()); 30 | canvas.drawRect(getBounds(), paint); 31 | } 32 | 33 | @Override 34 | public void setAlpha(int alpha) { 35 | paint.setAlpha(alpha); 36 | } 37 | 38 | @Override 39 | public void setColorFilter(@Nullable ColorFilter colorFilter) { 40 | paint.setColorFilter(colorFilter); 41 | } 42 | 43 | @Override 44 | public int getOpacity() { 45 | return PixelFormat.TRANSLUCENT; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/patches/theme/ThemePatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.patches.theme; 2 | 3 | import app.revanced.integrations.youtube.settings.Settings; 4 | import app.revanced.integrations.shared.Utils; 5 | import app.revanced.integrations.youtube.ThemeHelper; 6 | 7 | @SuppressWarnings("unused") 8 | public class ThemePatch { 9 | // color constants used in relation with litho components 10 | private static final int[] WHITE_VALUES = { 11 | -1, // comments chip background 12 | -394759, // music related results panel background 13 | -83886081, // video chapters list background 14 | }; 15 | 16 | private static final int[] DARK_VALUES = { 17 | -14145496, // explore drawer background 18 | -14606047, // comments chip background 19 | -15198184, // music related results panel background 20 | -15790321, // comments chip background (new layout) 21 | -98492127 // video chapters list background 22 | }; 23 | 24 | // background colors 25 | private static int whiteColor = 0; 26 | private static int blackColor = 0; 27 | 28 | // Used by app.revanced.patches.youtube.layout.theme.patch.LithoThemePatch 29 | /** 30 | * Change the color of Litho components. 31 | * If the color of the component matches one of the values, return the background color . 32 | * 33 | * @param originalValue The original color value. 34 | * @return The new or original color value 35 | */ 36 | public static int getValue(int originalValue) { 37 | if (ThemeHelper.isDarkTheme()) { 38 | if (anyEquals(originalValue, DARK_VALUES)) return getBlackColor(); 39 | } else { 40 | if (anyEquals(originalValue, WHITE_VALUES)) return getWhiteColor(); 41 | } 42 | return originalValue; 43 | } 44 | 45 | public static boolean gradientLoadingScreenEnabled() { 46 | return Settings.GRADIENT_LOADING_SCREEN.get(); 47 | } 48 | 49 | private static int getBlackColor() { 50 | if (blackColor == 0) blackColor = Utils.getResourceColor("yt_black1"); 51 | return blackColor; 52 | } 53 | 54 | private static int getWhiteColor() { 55 | if (whiteColor == 0) whiteColor = Utils.getResourceColor("yt_white1"); 56 | return whiteColor; 57 | } 58 | 59 | private static boolean anyEquals(int value, int... of) { 60 | for (int v : of) if (value == v) return true; 61 | return false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/requests/Route.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.requests; 2 | 3 | public class Route { 4 | private final String route; 5 | private final Method method; 6 | private final int paramCount; 7 | 8 | public 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 Method getMethod() { 18 | return method; 19 | } 20 | 21 | public 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 CompiledRoute(this, compiledRoute.toString()); 33 | } 34 | 35 | public static class CompiledRoute { 36 | private final Route baseRoute; 37 | private final String compiledRoute; 38 | 39 | private CompiledRoute(Route baseRoute, String compiledRoute) { 40 | this.baseRoute = baseRoute; 41 | this.compiledRoute = compiledRoute; 42 | } 43 | 44 | public String getCompiledRoute() { 45 | return compiledRoute; 46 | } 47 | 48 | public Method getMethod() { 49 | return baseRoute.method; 50 | } 51 | } 52 | 53 | private int countMatches(CharSequence seq, char c) { 54 | int count = 0; 55 | for (int i = 0; i < seq.length(); i++) { 56 | if (seq.charAt(i) == c) 57 | count++; 58 | } 59 | return count; 60 | } 61 | 62 | public enum Method { 63 | GET, 64 | POST 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.returnyoutubedislike.requests; 2 | 3 | import static app.revanced.integrations.youtube.requests.Route.Method.GET; 4 | import static app.revanced.integrations.youtube.requests.Route.Method.POST; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | 9 | import app.revanced.integrations.youtube.requests.Requester; 10 | import app.revanced.integrations.youtube.requests.Route; 11 | 12 | class ReturnYouTubeDislikeRoutes { 13 | static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/"; 14 | 15 | static final Route SEND_VOTE = new Route(POST, "interact/vote"); 16 | static final Route CONFIRM_VOTE = new Route(POST, "interact/confirmVote"); 17 | static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}"); 18 | static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}"); 19 | static final Route CONFIRM_REGISTRATION = new Route(POST, "puzzle/registration?userId={user_id}"); 20 | 21 | private ReturnYouTubeDislikeRoutes() { 22 | } 23 | 24 | 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/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 | public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | } 29 | public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | } 32 | public AlternativeThumbnailsAboutDeArrowPreference(Context context) { 33 | super(context); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/ForceAVCSpoofingPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import static app.revanced.integrations.shared.StringRef.str; 4 | import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.DEVICE_HAS_HARDWARE_DECODING_VP9; 5 | 6 | import android.content.Context; 7 | import android.preference.SwitchPreference; 8 | import android.util.AttributeSet; 9 | 10 | @SuppressWarnings({"unused", "deprecation"}) 11 | public class ForceAVCSpoofingPreference extends SwitchPreference { 12 | { 13 | if (!DEVICE_HAS_HARDWARE_DECODING_VP9) { 14 | setSummaryOn(str("revanced_spoof_video_streams_ios_force_avc_no_hardware_vp9_summary_on")); 15 | } 16 | } 17 | 18 | public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 19 | super(context, attrs, defStyleAttr, defStyleRes); 20 | } 21 | public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | public ForceAVCSpoofingPreference(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | } 27 | public ForceAVCSpoofingPreference(Context context) { 28 | super(context); 29 | } 30 | 31 | private void updateUI() { 32 | if (DEVICE_HAS_HARDWARE_DECODING_VP9) { 33 | return; 34 | } 35 | 36 | // Temporarily remove the preference key to allow changing this preference without 37 | // causing the settings UI listeners from showing reboot dialogs by the changes made here. 38 | String key = getKey(); 39 | setKey(null); 40 | 41 | // This setting cannot be changed by the user. 42 | super.setEnabled(false); 43 | super.setChecked(true); 44 | 45 | setKey(key); 46 | } 47 | 48 | @Override 49 | public void setEnabled(boolean enabled) { 50 | super.setEnabled(enabled); 51 | 52 | updateUI(); 53 | } 54 | 55 | @Override 56 | public void setChecked(boolean checked) { 57 | super.setChecked(checked); 58 | 59 | updateUI(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/HtmlPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import static android.text.Html.FROM_HTML_MODE_COMPACT; 4 | 5 | import android.content.Context; 6 | import android.os.Build; 7 | import android.preference.Preference; 8 | import android.text.Html; 9 | import android.util.AttributeSet; 10 | 11 | import androidx.annotation.RequiresApi; 12 | 13 | /** 14 | * Allows using basic html for the summary text. 15 | */ 16 | @SuppressWarnings({"unused", "deprecation"}) 17 | @RequiresApi(api = Build.VERSION_CODES.O) 18 | public class HtmlPreference extends Preference { 19 | { 20 | setSummary(Html.fromHtml(getSummary().toString(), FROM_HTML_MODE_COMPACT)); 21 | } 22 | 23 | public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 24 | super(context, attrs, defStyleAttr, defStyleRes); 25 | } 26 | public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | } 29 | public HtmlPreference(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | } 32 | public HtmlPreference(Context context) { 33 | super(context); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import android.os.Build; 4 | import android.preference.ListPreference; 5 | import android.preference.Preference; 6 | 7 | import androidx.annotation.RequiresApi; 8 | 9 | import app.revanced.integrations.shared.Logger; 10 | import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment; 11 | import app.revanced.integrations.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; 12 | import app.revanced.integrations.youtube.settings.Settings; 13 | 14 | /** 15 | * Preference fragment for ReVanced settings. 16 | * 17 | * @noinspection deprecation 18 | */ 19 | public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { 20 | 21 | @RequiresApi(api = Build.VERSION_CODES.O) 22 | @Override 23 | protected void initialize() { 24 | super.initialize(); 25 | 26 | try { 27 | // If the preference was included, then initialize it based on the available playback speed. 28 | Preference defaultSpeedPreference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key); 29 | if (defaultSpeedPreference instanceof ListPreference) { 30 | CustomPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference); 31 | } 32 | } catch (Exception ex) { 33 | Logger.printException(() -> "initialize failure", ex); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import app.revanced.integrations.shared.settings.preference.ReVancedAboutPreference; 7 | import app.revanced.integrations.youtube.ThemeHelper; 8 | 9 | @SuppressWarnings("unused") 10 | public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference { 11 | 12 | public int getLightColor() { 13 | return ThemeHelper.getLightThemeColor(); 14 | } 15 | 16 | public int getDarkColor() { 17 | return ThemeHelper.getDarkThemeColor(); 18 | } 19 | 20 | public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 21 | super(context, attrs, defStyleAttr, defStyleRes); 22 | } 23 | public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { 24 | super(context, attrs, defStyleAttr); 25 | } 26 | public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) { 27 | super(context, attrs); 28 | } 29 | public ReVancedYouTubeAboutPreference(Context context) { 30 | super(context); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/shared/PlayerControlsVisibilityObserver.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.shared 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import app.revanced.integrations.shared.Utils 7 | import java.lang.ref.WeakReference 8 | 9 | /** 10 | * default implementation of [PlayerControlsVisibilityObserver] 11 | * 12 | * @param activity activity that contains the controls_layout view 13 | */ 14 | class PlayerControlsVisibilityObserverImpl( 15 | private val activity: Activity, 16 | ) : PlayerControlsVisibilityObserver { 17 | 18 | /** 19 | * id of the direct parent of controls_layout, R.id.youtube_controls_overlay 20 | */ 21 | private val controlsLayoutParentId = 22 | Utils.getResourceIdentifier(activity, "youtube_controls_overlay", "id") 23 | 24 | /** 25 | * id of R.id.controls_layout 26 | */ 27 | private val controlsLayoutId = 28 | Utils.getResourceIdentifier(activity, "controls_layout", "id") 29 | 30 | /** 31 | * reference to the controls layout view 32 | */ 33 | private var controlsLayoutView = WeakReference(null) 34 | 35 | /** 36 | * is the [controlsLayoutView] set to a valid reference of a view? 37 | */ 38 | private val isAttached: Boolean 39 | get() { 40 | val view = controlsLayoutView.get() 41 | return view != null && view.parent != null 42 | } 43 | 44 | /** 45 | * find and attach the controls_layout view if needed 46 | */ 47 | private fun maybeAttach() { 48 | if (isAttached) return 49 | 50 | // find parent, then controls_layout view 51 | // this is needed because there may be two views where id=R.id.controls_layout 52 | // because why should google confine themselves to their own guidelines... 53 | activity.findViewById(controlsLayoutParentId)?.let { parent -> 54 | parent.findViewById(controlsLayoutId)?.let { 55 | controlsLayoutView = WeakReference(it) 56 | } 57 | } 58 | } 59 | 60 | override val playerControlsVisibility: Int 61 | get() { 62 | maybeAttach() 63 | return controlsLayoutView.get()?.visibility ?: View.GONE 64 | } 65 | 66 | override val arePlayerControlsVisible: Boolean 67 | get() = playerControlsVisibility == View.VISIBLE 68 | } 69 | 70 | /** 71 | * provides the visibility status of the fullscreen player controls_layout view. 72 | * this can be used for detecting when the player controls are shown 73 | */ 74 | interface PlayerControlsVisibilityObserver { 75 | /** 76 | * current visibility int of the controls_layout view 77 | */ 78 | val playerControlsVisibility: Int 79 | 80 | /** 81 | * is the value of [playerControlsVisibility] equal to [View.VISIBLE]? 82 | */ 83 | val arePlayerControlsVisible: Boolean 84 | } 85 | -------------------------------------------------------------------------------- /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.Logger 4 | import app.revanced.integrations.youtube.patches.VideoInformation 5 | 6 | /** 7 | * VideoState playback state. 8 | */ 9 | enum class VideoState { 10 | NEW, 11 | PLAYING, 12 | PAUSED, 13 | RECOVERABLE_ERROR, 14 | UNRECOVERABLE_ERROR, 15 | 16 | /** 17 | * @see [VideoInformation.isAtEndOfVideo] 18 | */ 19 | ENDED, 20 | 21 | ; 22 | 23 | companion object { 24 | 25 | private val nameToVideoState = values().associateBy { it.name } 26 | 27 | @JvmStatic 28 | fun setFromString(enumName: String) { 29 | val state = nameToVideoState[enumName] 30 | if (state == null) { 31 | Logger.printException { "Unknown VideoState encountered: $enumName" } 32 | } else if (currentVideoState != state) { 33 | Logger.printDebug { "VideoState changed to: $state" } 34 | currentVideoState = state 35 | } 36 | } 37 | 38 | /** 39 | * Depending on which hook this is called from, 40 | * this value may not be up to date with the actual playback state. 41 | */ 42 | @JvmStatic 43 | var current: VideoState? 44 | get() = currentVideoState 45 | private set(value) { 46 | currentVideoState = value 47 | } 48 | 49 | private var currentVideoState: VideoState? = null 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/youtube/sponsorblock/requests/SBRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.sponsorblock.requests; 2 | 3 | import static app.revanced.integrations.youtube.requests.Route.Method.GET; 4 | import static app.revanced.integrations.youtube.requests.Route.Method.POST; 5 | 6 | import app.revanced.integrations.youtube.requests.Route; 7 | 8 | class SBRoutes { 9 | static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}"); 10 | static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}"); 11 | static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}"); 12 | static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"ignoredSegmentCount\",\"viewCount\",\"minutesSaved\"]"); 13 | static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}"); 14 | 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 | static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}"); 16 | static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}"); 17 | 18 | private SBRoutes() { 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/swipecontrols/controller/AudioVolumeController.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.controller 2 | 3 | import android.content.Context 4 | import android.media.AudioManager 5 | import android.os.Build 6 | import app.revanced.integrations.shared.Logger.printException 7 | import app.revanced.integrations.youtube.swipecontrols.misc.clamp 8 | import kotlin.properties.Delegates 9 | 10 | /** 11 | * controller to adjust the device volume level 12 | * 13 | * @param context the context to bind the audio service in 14 | * @param targetStream the stream that is being controlled. Must be one of the STREAM_* constants in [AudioManager] 15 | */ 16 | class AudioVolumeController( 17 | context: Context, 18 | private val targetStream: Int = AudioManager.STREAM_MUSIC, 19 | ) { 20 | 21 | /** 22 | * audio service connection 23 | */ 24 | private lateinit var audioManager: AudioManager 25 | private var minimumVolumeIndex by Delegates.notNull() 26 | private var maximumVolumeIndex by Delegates.notNull() 27 | 28 | init { 29 | // bind audio service 30 | val mgr = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager 31 | if (mgr == null) { 32 | printException { "failed to acquire AUDIO_SERVICE" } 33 | } else { 34 | audioManager = mgr 35 | maximumVolumeIndex = audioManager.getStreamMaxVolume(targetStream) 36 | minimumVolumeIndex = 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 38 | audioManager.getStreamMinVolume( 39 | targetStream, 40 | ) 41 | } else { 42 | 0 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * the current volume, ranging from 0.0 to [maxVolume] 49 | */ 50 | var volume: Int 51 | get() { 52 | // check if initialized correctly 53 | if (!this::audioManager.isInitialized) return 0 54 | 55 | // get current volume 56 | return currentVolumeIndex - minimumVolumeIndex 57 | } 58 | set(value) { 59 | // check if initialized correctly 60 | if (!this::audioManager.isInitialized) return 61 | 62 | // set new volume 63 | currentVolumeIndex = 64 | (value + minimumVolumeIndex).clamp(minimumVolumeIndex, maximumVolumeIndex) 65 | } 66 | 67 | /** 68 | * the maximum possible volume 69 | */ 70 | val maxVolume: Int 71 | get() = maximumVolumeIndex - minimumVolumeIndex 72 | 73 | /** 74 | * the current volume index of the target stream 75 | */ 76 | private var currentVolumeIndex: Int 77 | get() = audioManager.getStreamVolume(targetStream) 78 | set(value) = audioManager.setStreamVolume(targetStream, value, 0) 79 | } 80 | -------------------------------------------------------------------------------- /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 | * is the screen brightness set to device- default? 27 | */ 28 | val isDefaultBrightness 29 | get() = (rawScreenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) 30 | 31 | /** 32 | * restore the screen brightness to the default device brightness 33 | */ 34 | fun restoreDefaultBrightness() { 35 | rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE 36 | } 37 | 38 | // Flag that indicates whether the brightness has been restored 39 | private var isBrightnessRestored = false 40 | 41 | /** 42 | * save the current screen brightness into settings, to be brought back using [restore] 43 | */ 44 | fun save() { 45 | if (isBrightnessRestored) { 46 | // Saves the current screen brightness value into settings 47 | host.config.savedScreenBrightnessValue = rawScreenBrightness 48 | // Reset the flag 49 | isBrightnessRestored = false 50 | } 51 | } 52 | 53 | /** 54 | * restore the screen brightness from settings saved using [save] 55 | */ 56 | fun restore() { 57 | // Restores the screen brightness value from the saved settings 58 | rawScreenBrightness = host.config.savedScreenBrightnessValue 59 | // Mark that brightness has been restored 60 | isBrightnessRestored = true 61 | } 62 | 63 | /** 64 | * wrapper for the raw screen brightness in [WindowManager.LayoutParams.screenBrightness] 65 | */ 66 | var rawScreenBrightness: Float 67 | get() = host.window.attributes.screenBrightness 68 | private set(value) { 69 | val attr = host.window.attributes 70 | attr.screenBrightness = value 71 | host.window.attributes = attr 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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 | KeyEvent.KEYCODE_VOLUME_UP -> 29 | handleVolumeKeyEvent(event, true) 30 | else -> false 31 | } 32 | } 33 | 34 | /** 35 | * handle a volume up / down key event 36 | * 37 | * @param event the key event 38 | * @param volumeUp was the key pressed the volume up key? 39 | * @return consume the event? 40 | */ 41 | private fun handleVolumeKeyEvent(event: KeyEvent, volumeUp: Boolean): Boolean { 42 | if (event.action == KeyEvent.ACTION_DOWN) { 43 | controller.audio?.apply { 44 | volume += if (volumeUp) 1 else -1 45 | controller.overlay.onVolumeChanged(volume, maxVolume) 46 | } 47 | } 48 | 49 | return true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/swipecontrols/controller/gesture/core/SwipeDetector.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.swipecontrols.controller.gesture.core 2 | 3 | import android.view.MotionEvent 4 | import kotlin.math.abs 5 | import kotlin.math.pow 6 | 7 | /** 8 | * describes a class that can detect swipes and their directionality 9 | */ 10 | interface SwipeDetector { 11 | /** 12 | * the currently detected swipe 13 | */ 14 | val currentSwipe: SwipeDirection 15 | 16 | /** 17 | * submit a onScroll event for swipe detection 18 | * 19 | * @param from start event 20 | * @param to end event 21 | * @param distanceX horizontal scroll distance 22 | * @param distanceY vertical scroll distance 23 | */ 24 | fun submitForSwipe( 25 | from: MotionEvent, 26 | to: MotionEvent, 27 | distanceX: Float, 28 | distanceY: Float, 29 | ) 30 | 31 | /** 32 | * reset the swipe detection 33 | */ 34 | fun resetSwipe() 35 | 36 | /** 37 | * direction of a swipe 38 | */ 39 | enum class SwipeDirection { 40 | /** 41 | * swipe has no direction or no swipe 42 | */ 43 | NONE, 44 | 45 | /** 46 | * swipe along the X- Axes 47 | */ 48 | HORIZONTAL, 49 | 50 | /** 51 | * swipe along the Y- Axes 52 | */ 53 | VERTICAL, 54 | } 55 | } 56 | 57 | /** 58 | * detector that can detect swipes and their directionality 59 | * 60 | * @param swipeMagnitudeThreshold minimum magnitude before a swipe is detected as such 61 | */ 62 | class SwipeDetectorImpl( 63 | private val swipeMagnitudeThreshold: Double, 64 | ) : SwipeDetector { 65 | override var currentSwipe = SwipeDetector.SwipeDirection.NONE 66 | 67 | override fun submitForSwipe( 68 | from: MotionEvent, 69 | to: MotionEvent, 70 | distanceX: Float, 71 | distanceY: Float, 72 | ) { 73 | if (currentSwipe == SwipeDetector.SwipeDirection.NONE) { 74 | // no swipe direction was detected yet, try to detect one 75 | // if the user did not swipe far enough, we cannot detect what direction they swiped 76 | // so we wait until a greater distance was swiped 77 | // NOTE: sqrt() can be high- cost, so using squared magnitudes here 78 | val deltaX = abs(to.x - from.x) 79 | val deltaY = abs(to.y - from.y) 80 | val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2) 81 | if (swipeMagnitudeSquared > swipeMagnitudeThreshold.pow(2)) { 82 | currentSwipe = if (deltaY > deltaX) { 83 | SwipeDetector.SwipeDirection.VERTICAL 84 | } else { 85 | SwipeDetector.SwipeDirection.HORIZONTAL 86 | } 87 | } 88 | } 89 | } 90 | 91 | override fun resetSwipe() { 92 | currentSwipe = SwipeDetector.SwipeDirection.NONE 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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/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/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: Int, 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/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/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 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/videoplayer/CopyVideoUrlButton.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.videoplayer; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.youtube.patches.CopyVideoUrlPatch; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.shared.Logger; 11 | 12 | @SuppressWarnings("unused") 13 | public class CopyVideoUrlButton extends PlayerControlButton { 14 | @Nullable 15 | private static CopyVideoUrlButton instance; 16 | 17 | public CopyVideoUrlButton(ViewGroup viewGroup) { 18 | super( 19 | viewGroup, 20 | "revanced_copy_video_url_button", 21 | Settings.COPY_VIDEO_URL, 22 | view -> CopyVideoUrlPatch.copyUrl(false), 23 | view -> { 24 | CopyVideoUrlPatch.copyUrl(true); 25 | return true; 26 | } 27 | ); 28 | } 29 | 30 | /** 31 | * Injection point. 32 | */ 33 | public static void initializeButton(View view) { 34 | try { 35 | instance = new CopyVideoUrlButton((ViewGroup) view); 36 | } catch (Exception ex) { 37 | Logger.printException(() -> "initializeButton failure", ex); 38 | } 39 | } 40 | 41 | /** 42 | * injection point 43 | */ 44 | public static void changeVisibilityImmediate(boolean visible) { 45 | if (instance != null) instance.setVisibilityImmediate(visible); 46 | } 47 | 48 | /** 49 | * injection point 50 | */ 51 | public static void changeVisibility(boolean visible, boolean animated) { 52 | if (instance != null) instance.setVisibility(visible, animated); 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/videoplayer/CopyVideoUrlTimestampButton.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.videoplayer; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.youtube.patches.CopyVideoUrlPatch; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.shared.Logger; 11 | 12 | @SuppressWarnings("unused") 13 | public class CopyVideoUrlTimestampButton extends PlayerControlButton { 14 | @Nullable 15 | private static CopyVideoUrlTimestampButton instance; 16 | 17 | public CopyVideoUrlTimestampButton(ViewGroup bottomControlsViewGroup) { 18 | super( 19 | bottomControlsViewGroup, 20 | "revanced_copy_video_url_timestamp_button", 21 | Settings.COPY_VIDEO_URL_TIMESTAMP, 22 | view -> CopyVideoUrlPatch.copyUrl(true), 23 | view -> { 24 | CopyVideoUrlPatch.copyUrl(false); 25 | return true; 26 | } 27 | ); 28 | } 29 | 30 | /** 31 | * Injection point. 32 | */ 33 | public static void initializeButton(View bottomControlsViewGroup) { 34 | try { 35 | instance = new CopyVideoUrlTimestampButton((ViewGroup) bottomControlsViewGroup); 36 | } catch (Exception ex) { 37 | Logger.printException(() -> "initializeButton failure", ex); 38 | } 39 | } 40 | 41 | /** 42 | * injection point 43 | */ 44 | public static void changeVisibilityImmediate(boolean visible) { 45 | if (instance != null) instance.setVisibilityImmediate(visible); 46 | } 47 | 48 | /** 49 | * injection point 50 | */ 51 | public static void changeVisibility(boolean visible, boolean animated) { 52 | if (instance != null) instance.setVisibility(visible, animated); 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/videoplayer/ExternalDownloadButton.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.videoplayer; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.shared.Logger; 9 | import app.revanced.integrations.youtube.patches.DownloadsPatch; 10 | import app.revanced.integrations.youtube.patches.VideoInformation; 11 | import app.revanced.integrations.youtube.settings.Settings; 12 | 13 | @SuppressWarnings("unused") 14 | public class ExternalDownloadButton extends PlayerControlButton { 15 | @Nullable 16 | private static ExternalDownloadButton instance; 17 | 18 | public ExternalDownloadButton(ViewGroup viewGroup) { 19 | super( 20 | viewGroup, 21 | "revanced_external_download_button", 22 | Settings.EXTERNAL_DOWNLOADER, 23 | ExternalDownloadButton::onDownloadClick, 24 | null 25 | ); 26 | } 27 | 28 | /** 29 | * Injection point. 30 | */ 31 | public static void initializeButton(View view) { 32 | try { 33 | instance = new ExternalDownloadButton((ViewGroup) view); 34 | } catch (Exception ex) { 35 | Logger.printException(() -> "initializeButton failure", ex); 36 | } 37 | } 38 | 39 | /** 40 | * injection point 41 | */ 42 | public static void changeVisibilityImmediate(boolean visible) { 43 | if (instance != null) instance.setVisibilityImmediate(visible); 44 | } 45 | 46 | /** 47 | * injection point 48 | */ 49 | public static void changeVisibility(boolean visible, boolean animated) { 50 | if (instance != null) instance.setVisibility(visible, animated); 51 | } 52 | 53 | private static void onDownloadClick(View view) { 54 | DownloadsPatch.launchExternalDownloader( 55 | VideoInformation.getVideoId(), 56 | view.getContext(), 57 | true); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/java/app/revanced/integrations/youtube/videoplayer/PlaybackSpeedDialogButton.java: -------------------------------------------------------------------------------- 1 | package app.revanced.integrations.youtube.videoplayer; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import app.revanced.integrations.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; 9 | import app.revanced.integrations.youtube.settings.Settings; 10 | import app.revanced.integrations.shared.Logger; 11 | 12 | @SuppressWarnings("unused") 13 | public class PlaybackSpeedDialogButton extends PlayerControlButton { 14 | @Nullable 15 | private static PlaybackSpeedDialogButton instance; 16 | 17 | public PlaybackSpeedDialogButton(ViewGroup viewGroup) { 18 | super( 19 | viewGroup, 20 | "revanced_playback_speed_dialog_button", 21 | Settings.PLAYBACK_SPEED_DIALOG_BUTTON, 22 | view -> CustomPlaybackSpeedPatch.showOldPlaybackSpeedMenu(), 23 | null 24 | ); 25 | } 26 | 27 | /** 28 | * Injection point. 29 | */ 30 | public static void initializeButton(View view) { 31 | try { 32 | instance = new PlaybackSpeedDialogButton((ViewGroup) view); 33 | } catch (Exception ex) { 34 | Logger.printException(() -> "initializeButton failure", ex); 35 | } 36 | } 37 | 38 | /** 39 | * injection point 40 | */ 41 | public static void changeVisibilityImmediate(boolean visible) { 42 | if (instance != null) instance.setVisibilityImmediate(visible); 43 | } 44 | 45 | /** 46 | * injection point 47 | */ 48 | public static void changeVisibility(boolean visible, boolean animated) { 49 | if (instance != null) instance.setVisibility(visible, animated); 50 | } 51 | } -------------------------------------------------------------------------------- /assets/revanced-logo/revanced-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.kotlin) apply false 5 | } 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel = true 2 | org.gradle.caching = true 3 | android.useAndroidX = true 4 | version = 1.16.0 5 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | #noinspection GradleDependency 3 | agp = "8.2.2" # 8.3.0 causes java verifier error: https://github.com/ReVanced/revanced-patches/issues/2818 4 | annotation = "1.8.0" 5 | kotlin = "2.0.0" 6 | appcompat = "1.7.0-rc01" 7 | okhttp = "5.0.0-alpha.14" 8 | retrofit = "2.11.0" 9 | 10 | [libraries] 11 | annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } 12 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 13 | okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } 14 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } 15 | 16 | [plugins] 17 | android-application = { id = "com.android.application", version.ref = "agp" } 18 | android-library = { id = "com.android.library", version.ref = "agp" } 19 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReVanced/revanced-integrations/53d5a94bfa16a74276c69252212ee6bc1a163027/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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.10.1", 7 | "semantic-release": "^24.1.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /stub/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | } 4 | 5 | android { 6 | namespace = "app.revanced.integrations.stub" 7 | compileSdk = 33 8 | 9 | defaultConfig { 10 | multiDexEnabled = false 11 | minSdk = 23 12 | } 13 | 14 | buildTypes { 15 | release { 16 | isMinifyEnabled = false 17 | proguardFiles( 18 | getDefaultProguardFile("proguard-android-optimize.txt"), 19 | "proguard-rules.pro", 20 | ) 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility = JavaVersion.VERSION_11 25 | targetCompatibility = JavaVersion.VERSION_11 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /stub/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /stub/src/main/java/android/support/constraint/ConstraintLayout.java: -------------------------------------------------------------------------------- 1 | package android.support.constraint; 2 | 3 | import android.content.Context; 4 | import android.view.ViewGroup; 5 | 6 | /** 7 | * "CompileOnly" class 8 | * because android.support.android.support.constraint.ConstraintLayout is deprecated 9 | * in favour of androidx.constraintlayout.widget.ConstraintLayout. 10 | * 11 | * This class will not be included and "replaced" by the real package's class. 12 | */ 13 | public class ConstraintLayout extends ViewGroup { 14 | public ConstraintLayout(Context context) { 15 | super(context); 16 | } 17 | 18 | @Override 19 | protected void onLayout(boolean changed, int l, int t, int r, int b) { } 20 | } 21 | -------------------------------------------------------------------------------- /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.View; 5 | 6 | public class RecyclerView extends View { 7 | 8 | public RecyclerView(Context context) { 9 | super(context); 10 | } 11 | 12 | public View getChildAt(@SuppressWarnings("unused") final int index) { 13 | return null; 14 | } 15 | 16 | public int getChildCount() { 17 | return 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stub/src/main/java/com/bytedance/ies/ugc/aweme/commercialize/compliance/personalization/AdPersonalizationActivity.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization; 2 | 3 | import android.app.Activity; 4 | 5 | //Dummy class 6 | public class AdPersonalizationActivity extends Activity { } 7 | -------------------------------------------------------------------------------- /stub/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.youtube.app.ui; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.ViewGroup; 6 | 7 | public class SlimMetadataScrollableButtonContainerLayout extends ViewGroup { 8 | 9 | public SlimMetadataScrollableButtonContainerLayout(Context context) { 10 | super(context); 11 | } 12 | 13 | public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs) { 14 | super(context, attrs); 15 | } 16 | 17 | public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 18 | super(context, attrs, defStyleAttr); 19 | } 20 | 21 | @Override 22 | protected void onLayout(boolean b, int i, int i1, int i2, int i3) { 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /stub/src/main/java/com/google/protos/youtube/api/innertube/InnertubeContext$ClientInfo.java: -------------------------------------------------------------------------------- 1 | package com.google.protos.youtube.api.innertube; 2 | 3 | public class InnertubeContext$ClientInfo { 4 | public int r; 5 | } 6 | -------------------------------------------------------------------------------- /stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.laurencedawson.reddit_sync.ui.activities; 2 | 3 | import android.app.Activity; 4 | 5 | public class WebViewActivity extends Activity { 6 | } 7 | -------------------------------------------------------------------------------- /stub/src/main/java/com/reddit/domain/model/ILink.java: -------------------------------------------------------------------------------- 1 | package com.reddit.domain.model; 2 | 3 | public class ILink { 4 | public boolean getPromoted() { 5 | throw new UnsupportedOperationException("Stub"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.rubenmayayo.reddit.ui.activities; 2 | 3 | import android.app.Activity; 4 | 5 | public class WebViewActivity extends Activity { 6 | } 7 | -------------------------------------------------------------------------------- /stub/src/main/java/com/ss/android/ugc/aweme/feed/model/Aweme.java: -------------------------------------------------------------------------------- 1 | package com.ss.android.ugc.aweme.feed.model; 2 | 3 | //Dummy class 4 | public class Aweme { 5 | public boolean isAd() { 6 | throw new UnsupportedOperationException("Stub"); 7 | } 8 | 9 | public boolean isLive() { 10 | throw new UnsupportedOperationException("Stub"); 11 | } 12 | 13 | public boolean isLiveReplay() { 14 | throw new UnsupportedOperationException("Stub"); 15 | } 16 | 17 | public boolean isWithPromotionalMusic() { 18 | throw new UnsupportedOperationException("Stub"); 19 | } 20 | 21 | public boolean getIsTikTokStory() { 22 | throw new UnsupportedOperationException("Stub"); 23 | } 24 | 25 | public boolean isImage() { 26 | throw new UnsupportedOperationException("Stub"); 27 | } 28 | 29 | public boolean isPhotoMode() { 30 | throw new UnsupportedOperationException("Stub"); 31 | } 32 | 33 | public AwemeStatistics getStatistics() { 34 | throw new UnsupportedOperationException("Stub"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /stub/src/main/java/com/ss/android/ugc/aweme/feed/model/AwemeStatistics.java: -------------------------------------------------------------------------------- 1 | package com.ss.android.ugc.aweme.feed.model; 2 | 3 | public class AwemeStatistics { 4 | public long getPlayCount() { 5 | throw new UnsupportedOperationException("Stub"); 6 | } 7 | public long getDiggCount() { 8 | throw new UnsupportedOperationException("Stub"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /stub/src/main/java/com/ss/android/ugc/aweme/feed/model/FeedItemList.java: -------------------------------------------------------------------------------- 1 | package com.ss.android.ugc.aweme.feed.model; 2 | 3 | import java.util.List; 4 | 5 | //Dummy class 6 | public class FeedItemList { 7 | public List items; 8 | } 9 | -------------------------------------------------------------------------------- /stub/src/main/java/com/tumblr/rumblr/model/TimelineObject.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.rumblr.model; 2 | 3 | public class TimelineObject { 4 | public final T getData() { 5 | throw new UnsupportedOperationException("Stub"); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /stub/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.rumblr.model; 2 | 3 | public enum TimelineObjectType { 4 | } 5 | -------------------------------------------------------------------------------- /stub/src/main/java/com/tumblr/rumblr/model/Timelineable.java: -------------------------------------------------------------------------------- 1 | package com.tumblr.rumblr.model; 2 | 3 | public interface Timelineable { 4 | TimelineObjectType getTimelineObjectType(); 5 | } 6 | -------------------------------------------------------------------------------- /stub/src/main/java/org/chromium/net/UrlRequest.java: -------------------------------------------------------------------------------- 1 | package org.chromium.net; 2 | 3 | public abstract class UrlRequest { 4 | public abstract class Builder { 5 | public abstract Builder addHeader(String name, String value); 6 | public abstract UrlRequest build(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /stub/src/main/java/tv/twitch/android/feature/settings/menu/SettingsMenuGroup.java: -------------------------------------------------------------------------------- 1 | package tv.twitch.android.feature.settings.menu; 2 | 3 | import java.util.List; 4 | 5 | // Dummy 6 | public final class SettingsMenuGroup { 7 | public SettingsMenuGroup(List settingsMenuItems) { 8 | throw new UnsupportedOperationException("Stub"); 9 | } 10 | 11 | public List getSettingsMenuItems() { 12 | throw new UnsupportedOperationException("Stub"); 13 | } 14 | } -------------------------------------------------------------------------------- /stub/src/main/java/tv/twitch/android/settings/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package tv.twitch.android.settings; 2 | 3 | import android.app.Activity; 4 | 5 | public class SettingsActivity extends Activity {} 6 | -------------------------------------------------------------------------------- /stub/src/main/java/tv/twitch/android/shared/chat/util/ClickableUsernameSpan.java: -------------------------------------------------------------------------------- 1 | package tv.twitch.android.shared.chat.util; 2 | 3 | import android.text.style.ClickableSpan; 4 | import android.view.View; 5 | 6 | public final class ClickableUsernameSpan extends ClickableSpan { 7 | @Override 8 | public void onClick(View widget) {} 9 | } --------------------------------------------------------------------------------