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: Add Disable analytics and telemetry patches #3448

Draft
wants to merge 21 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Some of the features the patches provide are:
* 🚫 **Block ads**: Say goodbye to ads
* ⭐ **Customize your app**: Personalize the appearance of apps with various layouts and themes
* 🪄 **Add new features**: Extend the functionality of apps with lots of new features
* 📡 **Protect your privacy**: Disable embedded trackers and privacy-invasive components
* ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions,
export activities, etc.
* ✨ **And much more!**
Expand Down
17 changes: 17 additions & 0 deletions api/revanced-patches.api
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ public final class app/revanced/patches/all/misc/versioncode/ChangeVersionCodePa
public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V
}

public final class app/revanced/patches/all/privacy/UniversalPrivacyPatch : app/revanced/patcher/patch/BytecodePatch {
public static final field INSTANCE Lapp/revanced/patches/all/privacy/UniversalPrivacyPatch;
public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V
public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V
}

public final class app/revanced/patches/all/privacy/UniversalResourcePrivacyPatch : app/revanced/patcher/patch/ResourcePatch {
public static final field INSTANCE Lapp/revanced/patches/all/privacy/UniversalResourcePrivacyPatch;
public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V
public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V
}

public final class app/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch {
public static final field INSTANCE Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch;
public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object;
Expand Down Expand Up @@ -1019,6 +1031,11 @@ public final class app/revanced/patches/shared/misc/settings/preference/TextPref
public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element;
}

public final class app/revanced/patches/shared/resource/AndroidManifest {
public static final field INSTANCE Lapp/revanced/patches/shared/resource/AndroidManifest;
public final fun addMetadata (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;)V
}

public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch : app/revanced/patcher/patch/BytecodePatch {
public static final field INSTANCE Lapp/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch;
public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package app.revanced.patches.all.privacy

import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.all.privacy.fingerprints.*
import app.revanced.util.resultOrThrow
import java.util.logging.Logger

@Patch(
name = "Disable analytics and telemetry",
description = "Patches the app to disable analytics and telemetry services.",
use = false,
)
@Suppress("unused")
object DisableAnalyticsAndTelemetryPatch : BytecodePatch(
setOf(
InitializeAsyncStatsigClientFingerprint,
InitAnalyticsFingerprint,
InitAppsFlyerSDKFingerprint,
SetupComsCoreFingerprint,
CallSettingsSpiFingerprint,
DoConfigFetchFingerprint,
SendFingerprint,
InitializeSdkFingerprint,
BuildSegmentFingerprint,
InitSDKFingerprint,
InitAmplitudeFingerprint,
),
) {
private val logger = Logger.getLogger(this::class.java.name)

private val disableGoogleAnalytics by option("Google Analytics")
private val disableAmplitude by option("Amplitude")
private val disableStatsig by option("Statsig")
private val disableAppsFlyerSDK by option("Apps Flyer SDK")
private val disableAppsFlyerPlugin by option("Apps Flyer plugin")
private val disableComsCore by option("ComsCore")
private val disableCrashlytics by option("Crashlytics")
private val disableFirebaseTransport by option("Firebase Transport")
private val disableMoEngage by option("MoEngage")
private val disableSegment by option("Segment")

override fun execute(context: BytecodeContext) = mapOf(
disableAmplitude to {
InitAmplitudeFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"return-object p0",
)
},
disableStatsig to {
InitializeAsyncStatsigClientFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"return-void",
)
},
disableAppsFlyerSDK to {
InitAppsFlyerSDKFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"return-object p0",
)
},
disableAppsFlyerPlugin to {
InitSDKFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"return-void",
)
},
disableComsCore to {
SetupComsCoreFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"return-void",
)
},
// Neutralize the method sending data to the backend.
disableFirebaseTransport to {
SendFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"return-void",
)
},
// Empties the "context" argument to force an exception.
disableGoogleAnalytics to {
InitAnalyticsFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"const/4 p0, 0x0",
)
},
// Empties the writeKey parameter to abort initialization
disableSegment to {
BuildSegmentFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"const-string p2, \"\"",
)
},
// Neutralize the two methods responsible for requesting Crashlytics' configuration.
// which effectively disables the SDK.
disableCrashlytics to {
CallSettingsSpiFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"""
const/4 p1, 0x0
return-object p1
""",
)

DoConfigFetchFingerprint.resultOrThrow().mutableMethod.apply {
// Jumps to the end of the method to directly return an empty object.
addInstructionsWithLabels(
0,
"""
goto :return_unit
""",
ExternalLabel("return_unit", getInstruction(getInstructions().lastIndex - 2)),
)
}
},
disableMoEngage to {
InitializeSdkFingerprint.resultOrThrow().mutableMethod.addInstructions(
0,
"""
const/4 v0, 0x0
return-object v0
""",
)
},
).forEach { (option, patch) ->
val isEnabled by option
if (!isEnabled!!) return@forEach

val message = try {
patch()

"Disabled ${option.title}"
} catch (exception: PatchException) {
"${option.title} was not found. Skipping."
}

logger.info(message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package app.revanced.patches.all.privacy

import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.util.asSequence
import app.revanced.util.getNode
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.util.logging.Logger

@Patch(
name = "Disable analytics and telemetry through manifest",
description = "Patches the manifest to disable analytics and telemetry services.",
use = false,
)
@Suppress("unused")
object DisableAnalyticsAndTelemetryThroughManifestPatch : ResourcePatch() {
private val logger = Logger.getLogger(this::class.java.name)

private val disableFirebaseTelemetry by option("Firebase telemetry")
Copy link
Member

Choose a reason for hiding this comment

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

I merged the two firebase patches to one.

private val disableFacebookSdk by option("Facebook SDK")
private val disableGoogleAnalytics by option("Google Analytics")
Comment on lines +22 to +24
Copy link
Member

Choose a reason for hiding this comment

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

GAnalytics patch also exists in the bytecode patch, why is it present here as well? Also, can't the firebase & facebook patches be converted to bytecode?


override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
mapOf(
disableFirebaseTelemetry to {
document.addMetadata(
"firebase_analytics_collection_enabled" to "false",
"firebase_analytics_collection_deactivated" to "true",
"firebase_crashlytics_collection_enabled" to "false",
"firebase_performance_collection_enabled" to "false",
"firebase_performance_collection_deactivated" to "true",
"firebase_data_collection_default_enabled" to "false",
)
},
disableFacebookSdk to {
document.addMetadata(
"com.facebook.sdk.AutoLogAppEventsEnabled" to "false",
"com.facebook.sdk.AdvertiserIDCollectionEnabled" to "false",
"com.facebook.sdk.MonitorEnabled" to "false",
"com.facebook.sdk.AutoInitEnabled" to "false",
)
},
disableGoogleAnalytics to {
document.addMetadata(
"google_analytics_adid_collection_enabled" to "false",
"google_analytics_default_allow_ad_personalization_signals" to "false",
"google_analytics_automatic_screen_reporting_enabled" to "false",
"google_analytics_default_allow_ad_storage" to "false",
"google_analytics_default_allow_ad_user_data" to "false",
"google_analytics_default_allow_analytics_storage" to "false",
"google_analytics_sgtm_upload_enabled" to "false",
"google_analytics_deferred_deep_link_enabled" to "false",
)
},
).forEach { option, patch ->
val isEnabled by option
if (!isEnabled!!) return@forEach

val message = try {
patch()

"Disabled ${option.title}"
} catch (exception: PatchException) {
"${option.title} was not found. Skipping."
}

logger.info(message)
}
}
}

private const val META_DATA_TAG = "meta-data"
private const val NAME_ATTRIBUTE = "android:name"
private const val VALUE_ATTRIBUTE = "android:value"

private fun Document.addMetadata(vararg metaDataNodes: Pair<String, String>) =
metaDataNodes.forEach { (nodeName, nodeValue) ->
val applicationNode = getNode("application") as Element
applicationNode.getElementsByTagName(META_DATA_TAG)
.asSequence()
.first { it.attributes.getNamedItem(NAME_ATTRIBUTE).nodeValue == nodeName }
Copy link
Member

Choose a reason for hiding this comment

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

I replaced firstOrNull with first here when refactoring. Correct this if it should be firstOrNull. If it should, then the try captch block above isn't necessary which was present before I refactored.

.attributes.getNamedItem(VALUE_ATTRIBUTE).nodeValue = nodeValue
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/app/revanced/patches/all/privacy/PatchExtension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package app.revanced.patches.all.privacy

import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
import kotlin.properties.ReadOnlyProperty

internal fun Patch<*>.option(
title: String,
) = ReadOnlyProperty<Any?, PatchOption<Boolean?>> { _, property ->
booleanPatchOption(key = property.name, default = true, title = title, required = true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package app.revanced.patches.all.privacy.fingerprints

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

internal object BuildSegmentFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
parameters = listOf("Landroid/content/Context;", "Ljava/lang/String;"),
strings = listOf("writeKey must not be empty."),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app.revanced.patches.all.privacy.fingerprints

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

internal object CallSettingsSpiFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC.value,
strings = listOf("Settings request failed."),
customFingerprint = { _, classDef ->
classDef.sourceFile == "DefaultSettingsSpiCall.java"
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.revanced.patches.all.privacy.fingerprints

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

// Matches is com.google.firebase.sessions.settings.RemoteSettingsFetcher.doConfigFetch.
internal object DoConfigFetchFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC.value,
parameters = listOf("Ljava/util/Map;", "L", "L", "L"),
customFingerprint = { _, classDef ->
classDef.sourceFile == "RemoteSettingsFetcher.kt"
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package app.revanced.patches.all.privacy.fingerprints

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

internal object InitAmplitudeFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.DECLARED_SYNCHRONIZED,
parameters = listOf("Landroid/content/Context;", "Ljava/lang/String;", "Ljava/lang/String;", "Ljava/lang/String;", "Z", "L"),
strings = listOf("Argument context cannot be null in initialize()", "Argument apiKey cannot be null or blank in initialize()"),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.revanced.patches.all.privacy.fingerprints

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

internal object InitAnalyticsFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
parameters = listOf("Landroid/content/Context;"),
strings = listOf("Slow initialization (ms)"),
customFingerprint = { _, classDef ->
classDef.sourceFile?.startsWith("com.google.android.gms:play-services-analytics-impl") == true
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app.revanced.patches.all.privacy.fingerprints

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

// Matches com.appsflyer.internal.AFb1vSDK.init.
internal object InitAppsFlyerSDKFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("L", "L", "L"),
strings = listOf("Initializing AppsFlyer SDK: (v%s.%s)"),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app.revanced.patches.all.privacy.fingerprints

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

internal object InitSDKFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PRIVATE.value,
parameters = listOf("L", "L"),
strings = listOf("manualStart", "afDevKey", "AF Dev Key is empty"),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app.revanced.patches.all.privacy.fingerprints

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

internal object InitializeAsyncStatsigClientFingerprint : MethodFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
customFingerprint = { methodDef, classDef ->
classDef.sourceFile == "StatsigClient.kt" && methodDef.name == "initializeAsync"
},
)
Loading
Loading