Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NU.nl): Add Hide ads and Spoof Certificate patch #4368

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions extensions/nunl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nunl:stub"))
}
1 change: 1 addition & 0 deletions extensions/nunl/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package app.revanced.extension.nunl.ads;

import nl.nu.performance.api.client.interfaces.Block;
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)",
oSumAtrIX marked this conversation as resolved.
Show resolved Hide resolved
"Aangeboden door NUshop"
};

// "Rubrieken" menu links to ads.
private static final String[] blockedLinkBlocks = {
"Van onze adverteerders"
};

public static void filterAds(List<Block> blocks) {
try {
ArrayList<Block> 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;
}
}
}
}

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) {
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
}
17 changes: 17 additions & 0 deletions extensions/nunl/stub/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions extensions/nunl/stub/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nl.nu.performance.api.client.interfaces;

public class Block {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nl.nu.performance.api.client.objects;

import nl.nu.performance.api.client.interfaces.Block;

public class DividerBlock extends Block {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nl.nu.performance.api.client.objects;

import nl.nu.performance.api.client.interfaces.Block;

public class DpgBannerBlock extends Block {

}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nl.nu.performance.api.client.objects;

public class Link {
public final StyledText getTitle() {
throw new UnsupportedOperationException("Stub");
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nl.nu.performance.api.client.objects;

public class StyledText {
public final String getText() {
throw new UnsupportedOperationException("Stub");
}
}
8 changes: 8 additions & 0 deletions patches/api/patches.api
Original file line number Diff line number Diff line change
Expand Up @@ -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/FirebasePatchKt {
public static final fun getFirebaseGetCertPatch ()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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 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"))

extendWith("extensions/nunl.rve")

execute {
// Disable video pre-roll ads.
LisoUseInAIKyrios marked this conversation as resolved.
Show resolved Hide resolved
// 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;-><init>()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<OneRegisterInstruction>(moveGetBlocksResultObjectIndex)

val listRegister = moveInstruction.registerA

// add instruction after moving List<Block> to register and then filter this List<Block> in place
addInstructions(
moveGetBlocksResultObjectIndex + 1,
"""
invoke-static { v$listRegister }, Lapp/revanced/extension/nunl/ads/HideAdsPatch;->filterAds(Ljava/util/List;)V
""",
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.revanced.patches.nunl.firebase

import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags

internal val getFingerprintHashForPackageFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE)
parameters()
returns("Ljava/lang/String;")

custom { methodDef, classDef ->
classDef.type.startsWith("Lcom/google/firebase/") && methodDef.name == "getFingerprintHashForPackage"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classDef already extends CharSequence, you can just call startsWith on it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was always intended to match 3 methods; adjusted to now have 3 signatures in separate classes and patch all 3

still relies on classDef.type now to check for equality of full class name, as many other fingerprints in this project do
that said, the documentation implies that one could check that as well without the .type which I think is incorrect or I'm doing something wrong

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 {
getFingerprintHashForPackageFingerprint.method.addInstructions(
0,
"""
const-string v0, "eae41fc018df2731a9b6ae1ac327da44a288667b"
return-object v0
""",
)
}
}