From f3268fb03ca25fb5465e36015b6c9dec2c84a655 Mon Sep 17 00:00:00 2001 From: Jasper Abbink Date: Thu, 27 Feb 2025 07:07:54 +0100 Subject: [PATCH] feat(NU.nl): Add `Hide ads` and `Spoof Certificate` patch (#4368) Co-authored-by: oSumAtrIX --- extensions/nunl/build.gradle.kts | 4 + extensions/nunl/src/main/AndroidManifest.xml | 1 + .../extension/nunl/ads/HideAdsPatch.java | 114 ++++++++++++++++++ extensions/nunl/stub/build.gradle.kts | 17 +++ .../nunl/stub/src/main/AndroidManifest.xml | 1 + .../api/client/interfaces/Block.java | 5 + .../api/client/objects/DividerBlock.java | 7 ++ .../api/client/objects/DpgBannerBlock.java | 7 ++ .../api/client/objects/HeaderBlock.java | 10 ++ .../performance/api/client/objects/Link.java | 13 ++ .../api/client/objects/LinkBlock.java | 10 ++ .../api/client/objects/StyledText.java | 7 ++ .../api/client/unions/LinkFlavor.java | 4 + .../client/unions/SmallArticleLinkFlavor.java | 7 ++ patches/api/patches.api | 8 ++ .../revanced/patches/nunl/ads/Fingerprints.kt | 44 +++++++ .../revanced/patches/nunl/ads/HideAdsPatch.kt | 51 ++++++++ .../app/revanced/patches/nunl/ads/Hooks.kt | 9 ++ .../patches/nunl/firebase/Fingerprints.kt | 20 +++ .../nunl/firebase/SpoofCertificatePatch.kt | 24 ++++ 20 files changed, 363 insertions(+) create mode 100644 extensions/nunl/build.gradle.kts create mode 100644 extensions/nunl/src/main/AndroidManifest.xml create mode 100644 extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java create mode 100644 extensions/nunl/stub/build.gradle.kts create mode 100644 extensions/nunl/stub/src/main/AndroidManifest.xml create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java create mode 100644 extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/nunl/ads/Hooks.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/nunl/firebase/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/nunl/firebase/SpoofCertificatePatch.kt diff --git a/extensions/nunl/build.gradle.kts b/extensions/nunl/build.gradle.kts new file mode 100644 index 000000000..6020de901 --- /dev/null +++ b/extensions/nunl/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:nunl:stub")) +} diff --git a/extensions/nunl/src/main/AndroidManifest.xml b/extensions/nunl/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/nunl/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java b/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java new file mode 100644 index 000000000..fb3cd0c54 --- /dev/null +++ b/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java @@ -0,0 +1,114 @@ +package app.revanced.extension.nunl.ads; + +import nl.nu.performance.api.client.interfaces.Block; +import nl.nu.performance.api.client.unions.SmallArticleLinkFlavor; +import nl.nu.performance.api.client.objects.*; + +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class HideAdsPatch { + private static final String[] blockedHeaderBlocks = { + "Aanbiedingen (Adverteerders)", + "Aangeboden door NUshop" + }; + + // "Rubrieken" menu links to ads. + private static final String[] blockedLinkBlocks = { + "Van onze adverteerders" + }; + + public static void filterAds(List blocks) { + try { + ArrayList cleanedList = new ArrayList<>(); + + boolean skipFullHeader = false; + boolean skipUntilDivider = false; + + int index = 0; + while (index < blocks.size()) { + Block currentBlock = blocks.get(index); + + // Because of pagination, we might not see the Divider in front of it. + // Just remove it as is and leave potential extra spacing visible on the screen. + if (currentBlock instanceof DpgBannerBlock) { + index++; + continue; + } + + if (index + 1 < blocks.size()) { + // Filter Divider -> DpgMediaBanner -> Divider. + if (currentBlock instanceof DividerBlock + && blocks.get(index + 1) instanceof DpgBannerBlock) { + index += 2; + continue; + } + + // Filter Divider -> LinkBlock (... -> LinkBlock -> LinkBlock-> LinkBlock -> Divider). + if (currentBlock instanceof DividerBlock + && blocks.get(index + 1) instanceof LinkBlock linkBlock) { + Link link = linkBlock.getLink(); + if (link != null && link.getTitle() != null) { + for (String blockedLinkBlock : blockedLinkBlocks) { + if (blockedLinkBlock.equals(link.getTitle().getText())) { + skipUntilDivider = true; + break; + } + } + if (skipUntilDivider) { + index++; + continue; + } + } + } + } + + // Skip LinkBlocks with a "flavor" claiming to be "isPartner" (sponsored inline ads). + if (currentBlock instanceof LinkBlock linkBlock + && linkBlock.getLink() != null + && linkBlock.getLink().getLinkFlavor() instanceof SmallArticleLinkFlavor smallArticleLinkFlavor + && smallArticleLinkFlavor.isPartner() != null + && smallArticleLinkFlavor.isPartner()) { + index++; + continue; + } + + if (currentBlock instanceof DividerBlock) { + skipUntilDivider = false; + } + + // Filter HeaderBlock with known ads until next HeaderBlock. + if (currentBlock instanceof HeaderBlock headerBlock) { + StyledText headerText = headerBlock.component20(); + if (headerText != null) { + skipFullHeader = false; + for (String blockedHeaderBlock : blockedHeaderBlocks) { + if (blockedHeaderBlock.equals(headerText.getText())) { + skipFullHeader = true; + break; + } + } + if (skipFullHeader) { + index++; + continue; + } + } + } + + if (!skipFullHeader && !skipUntilDivider) { + cleanedList.add(currentBlock); + } + index++; + } + + // Replace list in-place to not deal with moving the result to the correct register in smali. + blocks.clear(); + blocks.addAll(cleanedList); + } catch (Exception ex) { + Logger.printException(() -> "filterAds failure", ex); + } + } +} diff --git a/extensions/nunl/stub/build.gradle.kts b/extensions/nunl/stub/build.gradle.kts new file mode 100644 index 000000000..a8da923ed --- /dev/null +++ b/extensions/nunl/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id(libs.plugins.android.library.get().pluginId) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} diff --git a/extensions/nunl/stub/src/main/AndroidManifest.xml b/extensions/nunl/stub/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9b65eb06c --- /dev/null +++ b/extensions/nunl/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java new file mode 100644 index 000000000..3514f360c --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java @@ -0,0 +1,5 @@ +package nl.nu.performance.api.client.interfaces; + +public class Block { + +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java new file mode 100644 index 000000000..0351aec04 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.interfaces.Block; + +public class DividerBlock extends Block { + +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java new file mode 100644 index 000000000..ac300b053 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.interfaces.Block; + +public class DpgBannerBlock extends Block { + +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java new file mode 100644 index 000000000..f946b54da --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java @@ -0,0 +1,10 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.interfaces.Block; + +public class HeaderBlock extends Block { + // returns title + public final StyledText component20() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java new file mode 100644 index 000000000..771d11dad --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java @@ -0,0 +1,13 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.unions.LinkFlavor; + +public class Link { + public final StyledText getTitle() { + throw new UnsupportedOperationException("Stub"); + } + + public final LinkFlavor getLinkFlavor() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java new file mode 100644 index 000000000..dea195057 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java @@ -0,0 +1,10 @@ +package nl.nu.performance.api.client.objects; + +import android.os.Parcelable; +import nl.nu.performance.api.client.interfaces.Block; + +public abstract class LinkBlock extends Block implements Parcelable { + public final Link getLink() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java new file mode 100644 index 000000000..719403eb4 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.objects; + +public class StyledText { + public final String getText() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java new file mode 100644 index 000000000..08413d3fd --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java @@ -0,0 +1,4 @@ +package nl.nu.performance.api.client.unions; + +public interface LinkFlavor { +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java new file mode 100644 index 000000000..4dcbf23cb --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.unions; + +public class SmallArticleLinkFlavor implements LinkFlavor { + public final Boolean isPartner() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/patches/api/patches.api b/patches/api/patches.api index 2ac53b545..ef2702984 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -348,6 +348,14 @@ public final class app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatchKt { public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/nunl/ads/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/nunl/firebase/SpoofCertificatePatchKt { + public static final fun getSpoofCertificatePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/nyx/misc/pro/UnlockProPatchKt { public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt new file mode 100644 index 000000000..8332f2f24 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Fingerprints.kt @@ -0,0 +1,44 @@ +package app.revanced.patches.nunl.ads + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val jwUtilCreateAdvertisementFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + custom { methodDef, classDef -> + classDef.type == "Lnl/sanomamedia/android/nu/video/util/JWUtil;" && methodDef.name == "createAdvertising" + } +} + +internal val screenMapperFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Lnl/nu/android/bff/domain/models/screen/ScreenEntity;") + parameters("Lnl/nu/performance/api/client/objects/Screen;") + + opcodes( + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ, + Opcode.CHECK_CAST + ) + + custom { methodDef, classDef -> + classDef.type == "Lnl/nu/android/bff/data/mappers/ScreenMapper;" && methodDef.name == "map" + } +} + +internal val nextPageRepositoryImplFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("Lnl/nu/android/bff/domain/models/Page;") + parameters("Lnl/nu/performance/api/client/PacResponse;", "Ljava/lang/String;") + + opcodes( + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ, + Opcode.CHECK_CAST + ) + + custom { methodDef, classDef -> + classDef.type == "Lnl/nu/android/bff/data/repositories/NextPageRepositoryImpl;" && methodDef.name == "mapToPage" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt new file mode 100644 index 000000000..c09ce25e9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/HideAdsPatch.kt @@ -0,0 +1,51 @@ +package app.revanced.patches.nunl.ads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", + description = "Hide ads and sponsored articles in list pages and remove pre-roll ads on videos.", +) { + compatibleWith("nl.sanomamedia.android.nu"("11.0.0", "11.0.1", "11.1.0")) + + dependsOn(sharedExtensionPatch("nunl", mainActivityOnCreateHook)) + + execute { + // Disable video pre-roll ads. + // Whenever the app tries to create an ad via JWUtils.createAdvertising, don't actually tell the underlying JWPlayer library to do so => JWPlayer will not display ads. + jwUtilCreateAdvertisementFingerprint.method.addInstructions( + 0, + """ + new-instance v0, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder; + invoke-direct { v0 }, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;->()V + invoke-virtual { v0 }, Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig${'$'}Builder;->build()Lcom/jwplayer/pub/api/configuration/ads/VastAdvertisingConfig; + move-result-object v0 + return-object v0 + """, + ) + + // Filter injected content from API calls out of lists. + arrayOf(screenMapperFingerprint, nextPageRepositoryImplFingerprint).forEach { + // Index of instruction moving result of BlockPage;->getBlocks(...). + val moveGetBlocksResultObjectIndex = it.patternMatch!!.startIndex + it.method.apply { + val moveInstruction = getInstruction(moveGetBlocksResultObjectIndex) + + val listRegister = moveInstruction.registerA + + // Add instruction after moving List to register and then filter this List in place. + addInstructions( + moveGetBlocksResultObjectIndex + 1, + """ + invoke-static { v$listRegister }, Lapp/revanced/extension/nunl/ads/HideAdsPatch;->filterAds(Ljava/util/List;)V + """, + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Hooks.kt new file mode 100644 index 000000000..9a2e28bca --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nunl/ads/Hooks.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.nunl.ads + +import app.revanced.patches.shared.misc.extension.extensionHook + +internal val mainActivityOnCreateHook = extensionHook { + custom { method, classDef -> + classDef.type == "Lnl/sanomamedia/android/nu/main/NUMainActivity;" && method.name == "onCreate" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/firebase/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/firebase/Fingerprints.kt new file mode 100644 index 000000000..490819a4b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nunl/firebase/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.nunl.firebase + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val getFingerprintHashForPackageFingerprints = arrayOf( + "Lcom/google/firebase/installations/remote/FirebaseInstallationServiceClient;", + "Lcom/google/firebase/remoteconfig/internal/ConfigFetchHttpClient;", + "Lcom/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient;" +).map { className -> + fingerprint { + accessFlags(AccessFlags.PRIVATE) + parameters() + returns("Ljava/lang/String;") + + custom { methodDef, classDef -> + classDef.type == className && methodDef.name == "getFingerprintHashForPackage" + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nunl/firebase/SpoofCertificatePatch.kt b/patches/src/main/kotlin/app/revanced/patches/nunl/firebase/SpoofCertificatePatch.kt new file mode 100644 index 000000000..b87211251 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nunl/firebase/SpoofCertificatePatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.nunl.firebase + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val spoofCertificatePatch = bytecodePatch( + name = "Spoof certificate", + description = "Spoofs the X-Android-Cert header to allow push messages.", +) { + compatibleWith("nl.sanomamedia.android.nu") + + execute { + getFingerprintHashForPackageFingerprints.forEach { fingerprint -> + fingerprint.method.addInstructions( + 0, + """ + const-string v0, "eae41fc018df2731a9b6ae1ac327da44a288667b" + return-object v0 + """, + ) + } + } +}