feat(YouTube): add Spoof client patch

This commit is contained in:
inotia00 2024-05-26 16:51:15 +09:00
parent d8d3f52e7f
commit 0b9ea6ac26
18 changed files with 391 additions and 57 deletions

View File

@ -0,0 +1,5 @@
package app.revanced.patches.music.utils.fix.client
import app.revanced.patches.shared.spoofuseragent.BaseSpoofUserAgentPatch
object SpoofUserAgentPatch : BaseSpoofUserAgentPatch("com.google.android.apps.youtube.music")

View File

@ -1,5 +0,0 @@
package app.revanced.patches.music.utils.fix.clientspoof
import app.revanced.patches.shared.clientspoof.BaseClientSpoofPatch
object ClientSpoofPatch : BaseClientSpoofPatch("com.google.android.apps.youtube.music")

View File

@ -1,7 +1,7 @@
package app.revanced.patches.music.utils.gms
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.fix.clientspoof.ClientSpoofPatch
import app.revanced.patches.music.utils.fix.client.SpoofUserAgentPatch
import app.revanced.patches.music.utils.fix.fileprovider.FileProviderPatch
import app.revanced.patches.music.utils.integrations.IntegrationsPatch
import app.revanced.patches.music.utils.mainactivity.fingerprints.MainActivityFingerprint
@ -14,7 +14,7 @@ object GmsCoreSupportPatch : BaseGmsCoreSupportPatch(
fromPackageName = ORIGINAL_PACKAGE_NAME_YOUTUBE,
mainActivityOnCreateFingerprint = MainActivityFingerprint,
integrationsPatchDependency = IntegrationsPatch::class,
dependencies = setOf(ClientSpoofPatch::class, PackageNamePatch::class, FileProviderPatch::class),
dependencies = setOf(SpoofUserAgentPatch::class, PackageNamePatch::class, FileProviderPatch::class),
gmsCoreSupportResourcePatch = GmsCoreSupportResourcePatch,
compatiblePackages = COMPATIBLE_PACKAGE
)

View File

@ -0,0 +1,33 @@
package app.revanced.patches.shared.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfModelInstruction
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfReleaseInstruction
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
internal object CreatePlayerRequestBodyWithModelFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = emptyList(),
opcodes = listOf(Opcode.OR_INT_LIT16),
customFingerprint = { methodDef, _ ->
indexOfModelInstruction(methodDef) >= 0
&& indexOfReleaseInstruction(methodDef) >= 0
}
) {
fun indexOfModelInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
getReference<FieldReference>().toString() == "Landroid/os/Build;->MODEL:Ljava/lang/String;"
}
fun indexOfReleaseInstruction(methodDef: Method) =
methodDef.indexOfFirstInstruction {
getReference<FieldReference>().toString() == "Landroid/os/Build${'$'}VERSION;->RELEASE:Ljava/lang/String;"
}
}

View File

@ -4,10 +4,9 @@ import app.revanced.patcher.data.BytecodeContext
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.spoofappversion.fingerprints.ClientInfoFingerprint
import app.revanced.patches.shared.spoofappversion.fingerprints.ClientInfoParentFingerprint
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfReleaseInstruction
import app.revanced.util.getTargetIndexReversed
import app.revanced.util.getTargetIndexWithFieldReferenceName
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
@ -15,16 +14,12 @@ import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
abstract class BaseSpoofAppVersionPatch(
private val descriptor: String
) : BytecodePatch(
setOf(ClientInfoParentFingerprint)
setOf(CreatePlayerRequestBodyWithModelFingerprint)
) {
override fun execute(context: BytecodeContext) {
ClientInfoParentFingerprint.resultOrThrow().let { parentResult ->
ClientInfoFingerprint.resolve(context, parentResult.classDef)
ClientInfoFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val versionIndex = getTargetIndexWithFieldReferenceName("RELEASE") + 1
CreatePlayerRequestBodyWithModelFingerprint.resultOrThrow().mutableMethod.apply {
val versionIndex = indexOfReleaseInstruction(this) + 1
val insertIndex = getTargetIndexReversed(versionIndex, Opcode.IPUT_OBJECT)
val insertRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
@ -35,8 +30,6 @@ abstract class BaseSpoofAppVersionPatch(
"""
)
}
}
}
}
}

View File

@ -1,13 +0,0 @@
package app.revanced.patches.shared.spoofappversion.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 ClientInfoFingerprint : MethodFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = emptyList(),
opcodes = listOf(Opcode.OR_INT_LIT16)
)

View File

@ -1,8 +0,0 @@
package app.revanced.patches.shared.spoofappversion.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
internal object ClientInfoParentFingerprint : MethodFingerprint(
returnType = "V",
strings = listOf("Android Wear")
)

View File

@ -1,4 +1,4 @@
package app.revanced.patches.shared.clientspoof
package app.revanced.patches.shared.spoofuseragent
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
@ -16,7 +16,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
abstract class BaseClientSpoofPatch(
abstract class BaseSpoofUserAgentPatch(
private val packageName: String
) : BaseTransformInstructionsPatch<Instruction35cInfo>() {
override fun filterMap(

View File

@ -0,0 +1,239 @@
package app.revanced.patches.youtube.utils.fix.client
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfModelInstruction
import app.revanced.patches.youtube.utils.compatibility.Constants
import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildInitPlaybackRequestFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.BuildPlayerRequestURIFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.CreatePlayerRequestBodyFingerprint
import app.revanced.patches.youtube.utils.fix.client.fingerprints.SetPlayerRequestClientTypeFingerprint
import app.revanced.patches.youtube.utils.integrations.Constants.MISC_PATH
import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch
import app.revanced.patches.youtube.utils.settings.SettingsPatch
import app.revanced.patches.youtube.video.information.VideoInformationPatch
import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch
import app.revanced.util.getReference
import app.revanced.util.getStringInstructionIndex
import app.revanced.util.patch.BaseBytecodePatch
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
object SpoofClientPatch : BaseBytecodePatch(
name = "Spoof client",
description = "Adds options to spoofs the client to allow video playback.",
dependencies = setOf(
PlayerTypeHookPatch::class,
PlayerResponseMethodHookPatch::class,
SettingsPatch::class,
VideoInformationPatch::class,
SpoofUserAgentPatch::class,
),
compatiblePackages = Constants.COMPATIBLE_PACKAGE,
fingerprints = setOf(
BuildInitPlaybackRequestFingerprint,
BuildPlayerRequestURIFingerprint,
SetPlayerRequestClientTypeFingerprint,
CreatePlayerRequestBodyFingerprint,
CreatePlayerRequestBodyWithModelFingerprint,
)
) {
private const val INTEGRATIONS_CLASS_DESCRIPTOR =
"$MISC_PATH/SpoofClientPatch;"
private const val CLIENT_INFO_CLASS_DESCRIPTOR =
"Lcom/google/protos/youtube/api/innertube/InnertubeContext\$ClientInfo;"
override fun execute(context: BytecodeContext) {
// region Block /initplayback requests to fall back to /get_watch requests.
BuildInitPlaybackRequestFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val moveUriStringIndex = it.scanResult.patternScanResult!!.startIndex
val targetRegister = getInstruction<OneRegisterInstruction>(moveUriStringIndex).registerA
addInstructions(
moveUriStringIndex + 1, """
invoke-static { v$targetRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
move-result-object v$targetRegister
""",
)
}
}
// endregion
// region Block /get_watch requests to fall back to /player requests.
BuildPlayerRequestURIFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val invokeToStringIndex = it.scanResult.patternScanResult!!.startIndex
val uriRegister = getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC
addInstructions(
invokeToStringIndex, """
invoke-static { v$uriRegister }, $INTEGRATIONS_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
move-result-object v$uriRegister
""",
)
}
}
// endregion
// region Get field references to be used below.
val (clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField) =
SetPlayerRequestClientTypeFingerprint.resultOrThrow().let { result ->
with (result.mutableMethod) {
// Field in the player request object that holds the client info object.
val clientInfoField = getInstructions().find { instruction ->
// requestMessage.clientInfo = clientInfoBuilder.build();
instruction.opcode == Opcode.IPUT_OBJECT &&
instruction.getReference<FieldReference>()?.type == CLIENT_INFO_CLASS_DESCRIPTOR
}?.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoField")
// Client info object's client type field.
val clientInfoClientTypeField = getInstruction(result.scanResult.patternScanResult!!.endIndex)
.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoClientTypeField")
val clientInfoVersionIndex = getStringInstructionIndex("10.29")
val clientInfoVersionRegister = getInstruction<OneRegisterInstruction>(clientInfoVersionIndex).registerA
val clientInfoClientVersionFieldIndex = implementation!!.instructions.let {
clientInfoVersionIndex + it.subList(clientInfoVersionIndex, it.size - 1).indexOfFirst { instruction ->
instruction.opcode == Opcode.IPUT_OBJECT
&& (instruction as TwoRegisterInstruction).registerA == clientInfoVersionRegister
}
}
// Client info object's client version field.
val clientInfoClientVersionField = getInstruction(clientInfoClientVersionFieldIndex)
.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoClientVersionField")
Triple(clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField)
}
}
val clientInfoClientModelField = CreatePlayerRequestBodyWithModelFingerprint.resultOrThrow().mutableMethod.let {
val instructions = it.getInstructions()
val getClientModelIndex = indexOfModelInstruction(it)
// The next IPUT_OBJECT instruction after getting the client model is setting the client model field.
instructions.subList(
getClientModelIndex,
instructions.size,
).find { instruction ->
val reference = instruction.getReference<FieldReference>()
instruction.opcode == Opcode.IPUT_OBJECT
&& reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR
&& reference.type == "Ljava/lang/String;"
}?.getReference<FieldReference>() ?: throw PatchException("Could not find clientInfoClientModelField")
}
// endregion
// region Spoof client type for /player requests.
CreatePlayerRequestBodyFingerprint.resultOrThrow().let {
it.mutableMethod.apply {
val setClientInfoMethodName = "setClientInfo"
val checkCastIndex = it.scanResult.patternScanResult!!.startIndex
val checkCastInstruction = getInstruction<OneRegisterInstruction>(checkCastIndex)
val requestMessageInstanceRegister = checkCastInstruction.registerA
val clientInfoContainerClassName = checkCastInstruction.getReference<TypeReference>()!!.type
addInstruction(
checkCastIndex + 1,
"invoke-static { v$requestMessageInstanceRegister }," +
" $definingClass->$setClientInfoMethodName($clientInfoContainerClassName)V",
)
// Change client info to use the spoofed values.
// Do this in a helper method, to remove the need of picking out multiple free registers from the hooked code.
it.mutableClass.methods.add(
ImmutableMethod(
definingClass,
setClientInfoMethodName,
listOf(ImmutableMethodParameter(clientInfoContainerClassName, annotations, "clientInfoContainer")),
"V",
AccessFlags.PRIVATE or AccessFlags.STATIC,
annotations,
null,
MutableMethodImplementation(3),
).toMutable().apply {
addInstructions(
"""
invoke-static { }, $INTEGRATIONS_CLASS_DESCRIPTOR->isClientSpoofingEnabled()Z
move-result v0
if-eqz v0, :disabled
iget-object v0, p0, $clientInfoField
# Set client type to the spoofed value.
iget v1, v0, $clientInfoClientTypeField
invoke-static { v1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->getClientTypeId(I)I
move-result v1
iput v1, v0, $clientInfoClientTypeField
# Set client model to the spoofed value.
iget-object v1, v0, $clientInfoClientModelField
invoke-static { v1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->getClientModel(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $clientInfoClientModelField
# Set client version to the spoofed value.
iget-object v1, v0, $clientInfoClientVersionField
invoke-static { v1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
iput-object v1, v0, $clientInfoClientVersionField
:disabled
return-void
""",
)
},
)
}
}
// endregion
// region check whether video is Shorts or Clips.
PlayerResponseMethodHookPatch += PlayerResponseMethodHookPatch.Hook.PlayerParameter(
"$INTEGRATIONS_CLASS_DESCRIPTOR->setPlayerResponseVideoId(" +
"Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;",
)
// endregion
/**
* Add settings
*/
SettingsPatch.addPreference(
arrayOf(
"PREFERENCE_CATEGORY: MISC_EXPERIMENTAL_FLAGS",
"SETTINGS: SPOOF_CLIENT"
)
)
SettingsPatch.updatePatchStatus(this)
}
}

View File

@ -0,0 +1,5 @@
package app.revanced.patches.youtube.utils.fix.client
import app.revanced.patches.shared.spoofuseragent.BaseSpoofUserAgentPatch
object SpoofUserAgentPatch : BaseSpoofUserAgentPatch("com.google.android.youtube")

View File

@ -0,0 +1,16 @@
package app.revanced.patches.youtube.utils.fix.client.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal object BuildInitPlaybackRequestFingerprint : MethodFingerprint(
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
opcodes = listOf(
Opcode.MOVE_RESULT_OBJECT,
Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with.
),
strings = listOf(
"Content-Type",
"Range",
),
)

View File

@ -0,0 +1,20 @@
package app.revanced.patches.youtube.utils.fix.client.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal object BuildPlayerRequestURIFingerprint : MethodFingerprint(
returnType = "Ljava/lang/String;",
opcodes = listOf(
Opcode.INVOKE_VIRTUAL, // Register holds player request URI.
Opcode.MOVE_RESULT_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.IGET_OBJECT,
Opcode.MONITOR_EXIT,
Opcode.RETURN_OBJECT,
),
strings = listOf(
"key",
"asig",
),
)

View File

@ -0,0 +1,15 @@
package app.revanced.patches.youtube.utils.fix.client.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal object CreatePlayerRequestBodyFingerprint : MethodFingerprint(
returnType = "V",
parameters = listOf("L"),
opcodes = listOf(
Opcode.CHECK_CAST,
Opcode.IGET,
Opcode.AND_INT_LIT16,
),
strings = listOf("ms"),
)

View File

@ -0,0 +1,12 @@
package app.revanced.patches.youtube.utils.fix.client.fingerprints
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal object SetPlayerRequestClientTypeFingerprint : MethodFingerprint(
strings = listOf("10.29"),
opcodes = listOf(
Opcode.IGET,
Opcode.IPUT, // Sets ClientInfo.clientId.
),
)

View File

@ -1,5 +0,0 @@
package app.revanced.patches.youtube.utils.fix.clientspoof
import app.revanced.patches.shared.clientspoof.BaseClientSpoofPatch
object ClientSpoofPatch : BaseClientSpoofPatch("com.google.android.youtube")

View File

@ -4,7 +4,8 @@ import app.revanced.patches.shared.gms.BaseGmsCoreSupportPatch
import app.revanced.patches.shared.packagename.PackageNamePatch
import app.revanced.patches.shared.packagename.PackageNamePatch.ORIGINAL_PACKAGE_NAME_YOUTUBE
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.fix.clientspoof.ClientSpoofPatch
import app.revanced.patches.youtube.utils.fix.client.SpoofClientPatch
import app.revanced.patches.youtube.utils.fix.client.SpoofUserAgentPatch
import app.revanced.patches.youtube.utils.integrations.IntegrationsPatch
import app.revanced.patches.youtube.utils.mainactivity.fingerprints.MainActivityFingerprint
import app.revanced.patches.youtube.utils.settings.SettingsPatch
@ -14,7 +15,7 @@ object GmsCoreSupportPatch : BaseGmsCoreSupportPatch(
fromPackageName = ORIGINAL_PACKAGE_NAME_YOUTUBE,
mainActivityOnCreateFingerprint = MainActivityFingerprint,
integrationsPatchDependency = IntegrationsPatch::class,
dependencies = setOf(ClientSpoofPatch::class, PackageNamePatch::class, SettingsPatch::class),
dependencies = setOf(SpoofClientPatch::class, SpoofUserAgentPatch::class, PackageNamePatch::class, SettingsPatch::class),
gmsCoreSupportResourcePatch = GmsCoreSupportResourcePatch,
compatiblePackages = COMPATIBLE_PACKAGE
)

View File

@ -1414,6 +1414,27 @@ Tap on the continue button and disable battery optimizations."</string>
<string name="revanced_sanitize_sharing_links_summary">Removes tracking query parameters from the URLs when sharing links.</string>
<string name="revanced_disable_quic_protocol_title">Disable QUIC protocol</string>
<string name="revanced_disable_quic_protocol_summary">"Disable CronetEngine's QUIC protocol."</string>
<string name="revanced_spoof_client_title">Spoof client</string>
<string name="revanced_spoof_client_summary_on">"Client is spoofed.
Side effects include:
• No HDR video.
• Watch history may not work."</string>
<string name="revanced_spoof_client_summary_off">"Client is not spoofed. Video playback may not work."</string>
<string name="revanced_spoof_client_use_ios_title">Spoof client to iOS</string>
<string name="revanced_spoof_client_use_ios_summary_on">"Client is currently spoofed to iOS.
Side effects include:
• Playback speed menu is missing.
• Background playback does not work on live streams.
• Higher video qualities may be missing."</string>
<string name="revanced_spoof_client_use_ios_summary_off">"Client is currently spoofed to Android VR. (iOS client is used for Clips or Shorts)
Side effects include:
• Player swipe gestures do not work.
• Kids videos do not playback.
• Paused videos can randomly resume."</string>
<string name="revanced_spoof_player_parameter_title">Spoof player parameter</string>
<string name="revanced_spoof_player_parameter_summary">"Spoofs player parameters to prevent playback issues.

View File

@ -564,6 +564,10 @@
<!-- PREFERENCE_CATEGORY: MISC_EXPERIMENTAL_FLAGS
<PreferenceCategory android:title="@string/revanced_preference_category_experimental_flag" android:layout="@layout/revanced_settings_preferences_category"/>PREFERENCE_CATEGORY: MISC_EXPERIMENTAL_FLAGS -->
<!-- SETTINGS: SPOOF_CLIENT
<SwitchPreference android:title="@string/revanced_spoof_client_title" android:key="revanced_spoof_client" android:defaultValue="false" android:summaryOn="@string/revanced_spoof_client_summary_on" android:summaryOff="@string/revanced_spoof_client_summary_off" />
<SwitchPreference android:title="@string/revanced_spoof_client_use_ios_title" android:key="revanced_spoof_client_use_ios" android:defaultValue="true" android:summaryOn="@string/revanced_spoof_client_use_ios_summary_on" android:summaryOff="@string/revanced_spoof_client_use_ios_summary_off" />SETTINGS: SPOOF_CLIENT -->
<!-- SETTINGS: SPOOF_PLAYER_PARAMETER
<SwitchPreference android:title="@string/revanced_spoof_player_parameter_title" android:key="revanced_spoof_player_parameter" android:defaultValue="false" android:summary="@string/revanced_spoof_player_parameter_summary" />
<SwitchPreference android:title="@string/revanced_spoof_player_parameter_in_feed_title" android:key="revanced_spoof_player_parameter_in_feed" android:defaultValue="false" android:summaryOn="@string/revanced_spoof_player_parameter_in_feed_summary_on" android:summaryOff="@string/revanced_spoof_player_parameter_in_feed_summary_off" android:dependency="revanced_spoof_player_parameter" />SETTINGS: SPOOF_PLAYER_PARAMETER -->
@ -640,6 +644,7 @@
<Preference android:title="Enable minimized playback" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
<Preference android:title="Enable open links directly" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
<Preference android:title="Sanitize sharing links" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
<Preference android:title="Spoof client" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/revanced_preference_category_others" android:layout="@layout/revanced_settings_preferences_category">