diff --git a/api/revanced-patches.api b/api/revanced-patches.api index 4530ff7cbd..4b518d1d47 100644 --- a/api/revanced-patches.api +++ b/api/revanced-patches.api @@ -1050,6 +1050,12 @@ public final class app/revanced/patches/soundcloud/analytics/DisableTelemetryPat public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V } +public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch : app/revanced/patcher/patch/BytecodePatch { + public static final field INSTANCE Lapp/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch; + 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/spotify/layout/theme/CustomThemePatch : app/revanced/patcher/patch/ResourcePatch { public static final field INSTANCE Lapp/revanced/patches/spotify/layout/theme/CustomThemePatch; public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt b/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt index 402fa464f5..4f538458a3 100644 --- a/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt +++ b/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt @@ -10,7 +10,7 @@ import app.revanced.patcher.patch.annotation.CompatiblePackage import app.revanced.patcher.patch.annotation.Patch import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patches.soundcloud.ad.fingerprints.InterceptFingerprint -import app.revanced.patches.soundcloud.ad.fingerprints.FeatureConstructorFingerprint +import app.revanced.patches.soundcloud.shared.fingerprints.FeatureConstructorFingerprint import app.revanced.patches.soundcloud.ad.fingerprints.UserConsumerPlanConstructorFingerprint import app.revanced.util.resultOrThrow diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt b/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt new file mode 100644 index 0000000000..b6795e4a83 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt @@ -0,0 +1,83 @@ +package app.revanced.patches.soundcloud.offlinesync + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +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.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.soundcloud.offlinesync.fingerprints.DownloadOperationsHeaderVerificationFingerprint +import app.revanced.patches.soundcloud.offlinesync.fingerprints.DownloadOperationsURLBuilderFingerprint +import app.revanced.patches.soundcloud.shared.fingerprints.FeatureConstructorFingerprint +import app.revanced.util.getReference +import app.revanced.util.resultOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +@Patch( + name = "Enable offline sync", + compatiblePackages = [CompatiblePackage("com.soundcloud.android")], +) +@Suppress("unused") +object EnableOfflineSyncPatch : BytecodePatch( + setOf( + FeatureConstructorFingerprint, DownloadOperationsURLBuilderFingerprint, + DownloadOperationsHeaderVerificationFingerprint + ), +) { + override fun execute(context: BytecodeContext) { + // Enable the feature to allow offline track syncing by modifying the JSON server response. + // This method is the constructor of a class representing a "Feature" object parsed from JSON data. + // p1 is the name of the feature. + // p2 is true if the feature is enabled, false otherwise. + FeatureConstructorFingerprint.resultOrThrow().mutableMethod.apply { + val afterCheckNotNullIndex = 2 + + addInstructionsWithLabels( + afterCheckNotNullIndex, + """ + const-string v0, "offline_sync" + invoke-virtual { p1, v0 }, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + move-result v0 + if-eqz v0, :skip + const/4 p2, 0x1 + """, + ExternalLabel("skip", getInstruction(afterCheckNotNullIndex)), + ) + } + + // Patch the URL builder to use the HTTPS_STREAM endpoint + // instead of the offline sync endpoint to downloading the track. + DownloadOperationsURLBuilderFingerprint.resultOrThrow().mutableMethod.apply { + val getEndpointsEnumFieldIndex = 1 + val getEndpointsEnumFieldInstruction = getInstruction(getEndpointsEnumFieldIndex) + + val targetRegister = getEndpointsEnumFieldInstruction.registerA + val endpointsType = getEndpointsEnumFieldInstruction.getReference()!!.type + + replaceInstruction( + getEndpointsEnumFieldIndex, + "sget-object v$targetRegister, $endpointsType->HTTPS_STREAM:$endpointsType" + ) + } + + // The HTTPS_STREAM endpoint does not return the necessary headers for offline sync. + // Mock the headers to prevent the app from crashing by setting them to empty strings. + // The headers are all cosmetic and do not affect the functionality of the app. + DownloadOperationsHeaderVerificationFingerprint.resultOrThrow().mutableMethod.apply { + // The first three null checks need to be patched. + getInstructions().asSequence().filter { + it.opcode == Opcode.IF_EQZ + }.take(3).map { it.location.index }.forEach { nullCheckIndex -> + val headerStringRegister = getInstruction(nullCheckIndex).registerA + + addInstruction(nullCheckIndex, "const-string v$headerStringRegister, \"\"") + } + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/fingerprints/DownloadOperationsHeaderVerificationFingerprint.kt b/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/fingerprints/DownloadOperationsHeaderVerificationFingerprint.kt new file mode 100644 index 0000000000..e3629c788e --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/fingerprints/DownloadOperationsHeaderVerificationFingerprint.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.soundcloud.offlinesync.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object DownloadOperationsHeaderVerificationFingerprint : MethodFingerprint( + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L","L"), + opcodes = listOf( + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_STRING + ), + customFingerprint = { _, classDef -> + classDef.sourceFile == "DownloadOperations.kt" + } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/fingerprints/DownloadOperationsURLBuilderFingerprint.kt b/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/fingerprints/DownloadOperationsURLBuilderFingerprint.kt new file mode 100644 index 0000000000..5394658e54 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/fingerprints/DownloadOperationsURLBuilderFingerprint.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.soundcloud.offlinesync.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object DownloadOperationsURLBuilderFingerprint : MethodFingerprint( + returnType = "Ljava/lang/String", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L","L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.SGET_OBJECT, + Opcode.FILLED_NEW_ARRAY + ), + customFingerprint = { _, classDef -> + classDef.sourceFile == "DownloadOperations.kt" + } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/ad/fingerprints/FeatureConstructorFingerprint.kt b/src/main/kotlin/app/revanced/patches/soundcloud/shared/fingerprints/FeatureConstructorFingerprint.kt similarity index 88% rename from src/main/kotlin/app/revanced/patches/soundcloud/ad/fingerprints/FeatureConstructorFingerprint.kt rename to src/main/kotlin/app/revanced/patches/soundcloud/shared/fingerprints/FeatureConstructorFingerprint.kt index c6d93f491e..6506ebab2c 100644 --- a/src/main/kotlin/app/revanced/patches/soundcloud/ad/fingerprints/FeatureConstructorFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/soundcloud/shared/fingerprints/FeatureConstructorFingerprint.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.soundcloud.ad.fingerprints +package app.revanced.patches.soundcloud.shared.fingerprints import app.revanced.patcher.extensions.or import app.revanced.patcher.fingerprint.MethodFingerprint