diff --git a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt index cf5b0b816..608dccca4 100644 --- a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt +++ b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt @@ -25,7 +25,7 @@ internal data class ArrayResource( resourceCallback?.invoke(item) this.appendChild(ownerDocument.createElement("item").also { itemNode -> - itemNode.textContent = item.value + itemNode.textContent = "@string/${item.name}" }) } } diff --git a/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt index c8197a1a1..f62c24b21 100644 --- a/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt +++ b/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt @@ -92,7 +92,7 @@ abstract class AbstractSettingsResourcePatch( * @param arrayResource The array resource to add. */ fun addArray(arrayResource: ArrayResource) = - arraysNode!!.addResource(arrayResource) + arraysNode!!.addResource(arrayResource) { it.include() } /** * Add a preference to the settings. diff --git a/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/annotations/EmbeddedAdsCompatibility.kt b/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/annotations/EmbeddedAdsCompatibility.kt new file mode 100644 index 000000000..4103c9405 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/annotations/EmbeddedAdsCompatibility.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.twitch.ad.embedded.annotations + +import app.revanced.patcher.annotation.Compatibility +import app.revanced.patcher.annotation.Package + +@Compatibility([Package("tv.twitch.android.app")]) +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +internal annotation class EmbeddedAdsCompatibility + diff --git a/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/fingerprints/CreateUsherClientFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/fingerprints/CreateUsherClientFingerprint.kt new file mode 100644 index 000000000..348044040 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/fingerprints/CreateUsherClientFingerprint.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.twitch.ad.embedded.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint + +object CreateUsherClientFingerprint : MethodFingerprint( + customFingerprint = { method -> + method.definingClass.endsWith("Ltv/twitch/android/network/OkHttpClientFactory;") && method.name == "buildOkHttpClient" + } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/patch/EmbeddedAdsPatch.kt b/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/patch/EmbeddedAdsPatch.kt new file mode 100644 index 000000000..cf8f53e63 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/patch/EmbeddedAdsPatch.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.twitch.ad.embedded.patch + +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.MethodFingerprintExtensions.name +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultError +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.patch.annotations.DependsOn +import app.revanced.patcher.patch.annotations.Patch +import app.revanced.patches.shared.settings.preference.impl.ArrayResource +import app.revanced.patches.shared.settings.preference.impl.ListPreference +import app.revanced.patches.shared.settings.preference.impl.StringResource +import app.revanced.patches.twitch.ad.embedded.annotations.EmbeddedAdsCompatibility +import app.revanced.patches.twitch.ad.embedded.fingerprints.CreateUsherClientFingerprint +import app.revanced.patches.twitch.ad.video.patch.VideoAdsPatch +import app.revanced.patches.twitch.misc.integrations.patch.IntegrationsPatch +import app.revanced.patches.twitch.misc.settings.bytecode.patch.SettingsPatch + +@Patch +@DependsOn([VideoAdsPatch::class, IntegrationsPatch::class, SettingsPatch::class]) +@Name("block-embedded-ads") +@Description("Blocks embedded steam ads using services like TTV.lol or PurpleAdBlocker.") +@EmbeddedAdsCompatibility +@Version("0.0.1") +class EmbeddedAdsPatch : BytecodePatch( + listOf(CreateUsherClientFingerprint) +) { + override fun execute(context: BytecodeContext): PatchResult { + val result = CreateUsherClientFingerprint.result ?: return PatchResultError("${CreateUsherClientFingerprint.name} not found") + + // Inject OkHttp3 application interceptor + result.mutableMethod.addInstructions( + 3, + """ + invoke-static {}, Lapp/revanced/twitch/patches/EmbeddedAdsPatch;->createRequestInterceptor()Lapp/revanced/twitch/api/RequestInterceptor; + move-result-object v2 + invoke-virtual {v0, v2}, Lokhttp3/OkHttpClient${"$"}Builder;->addInterceptor(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient${"$"}Builder; + """ + ) + + SettingsPatch.PreferenceScreen.ADS.SURESTREAM.addPreferences( + ListPreference( + "revanced_block_embedded_ads", + StringResource( + "revanced_block_embedded_ads", + "Block embedded video ads" + ), + ArrayResource( + "revanced_hls_proxies", + listOf( + StringResource("revanced_proxy_disabled", "Disabled"), + StringResource("revanced_proxy_ttv_lol", "TTV LOL proxy"), + StringResource("revanced_proxy_purpleadblock", "PurpleAdBlock proxy"), + ) + ), + ArrayResource( + "revanced_hls_proxies_values", + listOf( + StringResource("key_revanced_proxy_disabled", "disabled"), + StringResource("key_revanced_proxy_ttv_lol", "ttv-lol"), + StringResource("key_revanced_proxy_purpleadblock", "purpleadblock") + ) + ), + "ttv-lol" + ) + ) + + SettingsPatch.addString("revanced_embedded_ads_service_unavailable", "%s is unavailable. Ads may show. Try switching to another ad block service in settings.") + SettingsPatch.addString("revanced_embedded_ads_service_failed", "%s server returned an error. Ads may show. Try switching to another ad block service in settings.") + + return PatchResultSuccess() + } +} diff --git a/src/main/kotlin/app/revanced/patches/twitch/ad/shared/util/AbstractAdPatch.kt b/src/main/kotlin/app/revanced/patches/twitch/ad/shared/util/AbstractAdPatch.kt new file mode 100644 index 000000000..dca265bc7 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/ad/shared/util/AbstractAdPatch.kt @@ -0,0 +1,51 @@ +package app.revanced.patches.twitch.ad.shared.util + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.instruction +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel + +abstract class AbstractAdPatch( + val conditionCall: String, + val skipLabelName: String, + internal val fingerprints: Iterable? = null, +) : BytecodePatch(fingerprints) { + + protected fun createConditionInstructions(register: String = "v0") = """ + invoke-static { }, $conditionCall + move-result $register + if-eqz $register, :$skipLabelName + """ + + protected data class ReturnMethod(val returnType: Char = 'V', val value: String = "") + + protected fun BytecodeContext.blockMethods(clazz: String, vararg methodNames: String, returnMethod: ReturnMethod = ReturnMethod()): Boolean { + + return with(findClass(clazz)?.mutableClass) { + this ?: return false + + this.methods.filter { methodNames.contains(it.name) }.forEach { + val retIntructions = when(returnMethod.returnType) { + 'V' -> "return-void" + 'Z' -> """ + const/4 v0, ${returnMethod.value} + return v0 + """ + else -> throw NotImplementedError() + } + it.addInstructions( + 0, + """ + ${createConditionInstructions("v0")} + $retIntructions + """, + listOf(ExternalLabel(skipLabelName, it.instruction(0))) + ) + } + true + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitch/ad/video/fingerprints/AdsManagerFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitch/ad/video/fingerprints/GetReadyToShowAdFingerprint.kt similarity index 50% rename from src/main/kotlin/app/revanced/patches/twitch/ad/video/fingerprints/AdsManagerFingerprint.kt rename to src/main/kotlin/app/revanced/patches/twitch/ad/video/fingerprints/GetReadyToShowAdFingerprint.kt index 770192cd0..3256ad024 100644 --- a/src/main/kotlin/app/revanced/patches/twitch/ad/video/fingerprints/AdsManagerFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/twitch/ad/video/fingerprints/GetReadyToShowAdFingerprint.kt @@ -1,10 +1,9 @@ package app.revanced.patches.twitch.ad.video.fingerprints - import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -object AdsManagerFingerprint : MethodFingerprint( +object GetReadyToShowAdFingerprint : MethodFingerprint( customFingerprint = { method -> - method.definingClass.endsWith("AdsManagerImpl;") && method.name == "playAds" + method.definingClass.endsWith("/StreamDisplayAdsPresenter;") && method.name == "getReadyToShowAdOrAbort" } ) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitch/ad/video/patch/VideoAdsPatch.kt b/src/main/kotlin/app/revanced/patches/twitch/ad/video/patch/VideoAdsPatch.kt index 5f2acf66d..9b1197e78 100644 --- a/src/main/kotlin/app/revanced/patches/twitch/ad/video/patch/VideoAdsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/twitch/ad/video/patch/VideoAdsPatch.kt @@ -6,7 +6,6 @@ import app.revanced.patcher.annotation.Version import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.instruction -import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResultSuccess import app.revanced.patcher.patch.annotations.DependsOn @@ -14,10 +13,11 @@ import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patches.shared.settings.preference.impl.StringResource import app.revanced.patches.shared.settings.preference.impl.SwitchPreference +import app.revanced.patches.twitch.ad.shared.util.AbstractAdPatch import app.revanced.patches.twitch.ad.video.annotations.VideoAdsCompatibility -import app.revanced.patches.twitch.ad.video.fingerprints.AdsManagerFingerprint import app.revanced.patches.twitch.ad.video.fingerprints.CheckAdEligibilityLambdaFingerprint import app.revanced.patches.twitch.ad.video.fingerprints.ContentConfigShowAdsFingerprint +import app.revanced.patches.twitch.ad.video.fingerprints.GetReadyToShowAdFingerprint import app.revanced.patches.twitch.misc.integrations.patch.IntegrationsPatch import app.revanced.patches.twitch.misc.settings.bytecode.patch.SettingsPatch @@ -27,20 +27,63 @@ import app.revanced.patches.twitch.misc.settings.bytecode.patch.SettingsPatch @Description("Blocks video ads in streams and VODs.") @VideoAdsCompatibility @Version("0.0.1") -class VideoAdsPatch : BytecodePatch( +class VideoAdsPatch : AbstractAdPatch( + "Lapp/revanced/twitch/patches/VideoAdsPatch;->shouldBlockVideoAds()Z", + "show_video_ads", listOf( ContentConfigShowAdsFingerprint, - AdsManagerFingerprint, - CheckAdEligibilityLambdaFingerprint + CheckAdEligibilityLambdaFingerprint, + GetReadyToShowAdFingerprint ) ) { - private fun createConditionInstructions(register: String = "v0") = """ - invoke-static { }, Lapp/revanced/twitch/patches/VideoAdsPatch;->shouldBlockVideoAds()Z - move-result $register - if-eqz $register, :show_video_ads - """ - override fun execute(context: BytecodeContext): PatchResult { + /* Amazon ads SDK */ + context.blockMethods( + "Lcom/amazon/ads/video/player/AdsManagerImpl;", + "playAds" + ) + + /* Twitch ads manager */ + context.blockMethods( + "Ltv/twitch/android/shared/ads/VideoAdManager;", + "checkAdEligibilityAndRequestAd", "requestAd", "requestAds" + ) + + /* Various ad presenters */ + context.blockMethods( + "Ltv/twitch/android/shared/ads/AdsPlayerPresenter;", + "requestAd", "requestFirstAd", "requestFirstAdIfEligible", "requestMidroll", "requestAdFromMultiAdFormatEvent" + ) + + context.blockMethods( + "Ltv/twitch/android/shared/ads/AdsVodPlayerPresenter;", + "requestAd", "requestFirstAd", + ) + + context.blockMethods( + "Ltv/twitch/android/feature/theatre/ads/AdEdgeAllocationPresenter;", + "parseAdAndCheckEligibility", "requestAdsAfterEligibilityCheck", "showAd", "bindMultiAdFormatAllocation" + ) + + /* A/B ad testing experiments */ + context.blockMethods( + "Ltv/twitch/android/provider/experiments/helpers/DisplayAdsExperimentHelper;", + "areDisplayAdsEnabled", + returnMethod = ReturnMethod('Z', "0") + ) + + context.blockMethods( + "Ltv/twitch/android/shared/ads/tracking/MultiFormatAdsTrackingExperiment;", + "shouldUseMultiAdFormatTracker", "shouldUseVideoAdTracker", + returnMethod = ReturnMethod('Z', "0") + ) + + context.blockMethods( + "Ltv/twitch/android/shared/ads/MultiformatAdsExperiment;", + "shouldDisableClientSideLivePreroll", "shouldDisableClientSideVodPreroll", + returnMethod = ReturnMethod('Z', "1") + ) + // Pretend our player is ineligible for all ads with(CheckAdEligibilityLambdaFingerprint.result!!) { mutableMethod.addInstructions( @@ -52,7 +95,22 @@ class VideoAdsPatch : BytecodePatch( move-result-object p0 return-object p0 """, - listOf(ExternalLabel("show_video_ads", mutableMethod.instruction(0))) + listOf(ExternalLabel(skipLabelName, mutableMethod.instruction(0))) + ) + } + + with(GetReadyToShowAdFingerprint.result!!) { + val adFormatDeclined = "Ltv/twitch/android/shared/display/ads/theatre/StreamDisplayAdsPresenter\$Action\$AdFormatDeclined;" + mutableMethod.addInstructions( + 0, + """ + ${createConditionInstructions()} + sget-object p2, $adFormatDeclined->INSTANCE:$adFormatDeclined + invoke-static {p1, p2}, Ltv/twitch/android/core/mvp/presenter/StateMachineKt;->plus(Ltv/twitch/android/core/mvp/presenter/PresenterState;Ltv/twitch/android/core/mvp/presenter/PresenterAction;)Ltv/twitch/android/core/mvp/presenter/StateAndAction; + move-result-object p1 + return-object p1 + """, + listOf(ExternalLabel(skipLabelName, mutableMethod.instruction(0))) ) } @@ -61,24 +119,12 @@ class VideoAdsPatch : BytecodePatch( mutableMethod.addInstructions(0, """ ${createConditionInstructions()} const/4 v0, 0 - :show_video_ads + :$skipLabelName return v0 """ ) } - // Block playAds call - with(AdsManagerFingerprint.result!!) { - mutableMethod.addInstructions( - 0, - """ - ${createConditionInstructions()} - return-void - """, - listOf(ExternalLabel("show_video_ads", mutableMethod.instruction(0))) - ) - } - SettingsPatch.PreferenceScreen.ADS.CLIENT_SIDE.addPreferences( SwitchPreference( "revanced_block_video_ads", diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt index e2d0e51e5..d0f8b7214 100644 --- a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt @@ -173,6 +173,7 @@ class SettingsPatch : BytecodePatch( val GENERAL = CustomCategory("general", "General settings") val OTHER = CustomCategory("other", "Other settings") val CLIENT_SIDE = CustomCategory("client_ads", "Client-side ads") + val SURESTREAM = CustomCategory("surestream_ads", "Server-side surestream ads") internal inner class CustomCategory(key: String, title: String) : Screen.Category(key, title) { /* For Twitch, we need to load our CustomPreferenceCategory class instead of the default one. */