Merge dev

This commit is contained in:
oSumAtrIX 2026-01-30 18:01:01 +01:00
commit 48b3a2d18c
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
183 changed files with 62850 additions and 59577 deletions

View file

@ -0,0 +1,54 @@
package app.revanced.patches.all.misc.screenshot
import app.revanced.patcher.extensions.removeInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
import app.revanced.util.getReference
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil
private val registerScreenCaptureCallbackMethodReference = ImmutableMethodReference(
"Landroid/app/Activity;",
"registerScreenCaptureCallback",
listOf(
"Ljava/util/concurrent/Executor;",
"Landroid/app/Activity\$ScreenCaptureCallback;",
),
"V",
)
private val unregisterScreenCaptureCallbackMethodReference = ImmutableMethodReference(
"Landroid/app/Activity;",
"unregisterScreenCaptureCallback",
listOf(
"Landroid/app/Activity\$ScreenCaptureCallback;",
),
"V",
)
@Suppress("unused")
val preventScreenshotDetectionPatch = bytecodePatch(
name = "Prevent screenshot detection",
description = "Removes the registration of all screen capture callbacks. This prevents the app from detecting screenshots.",
use = false,
) {
dependsOn(
transformInstructionsPatch(
filterMap = { _, _, instruction, instructionIndex ->
if (instruction.opcode != Opcode.INVOKE_VIRTUAL) return@transformInstructionsPatch null
val reference = instruction.getReference<MethodReference>() ?: return@transformInstructionsPatch null
instructionIndex.takeIf {
MethodUtil.methodSignaturesMatch(reference, registerScreenCaptureCallbackMethodReference) ||
MethodUtil.methodSignaturesMatch(reference, unregisterScreenCaptureCallbackMethodReference)
}
},
transform = { mutableMethod, instructionIndex ->
mutableMethod.removeInstruction(instructionIndex)
},
),
)
}

View file

@ -0,0 +1,12 @@
package app.revanced.patches.instagram.hide.highlightsTray
import app.revanced.patcher.composingFirstMethod
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.patch.BytecodePatchContext
internal const val TARGET_STRING = "highlights_tray"
internal val BytecodePatchContext.highlightsUrlBuilderMethodMatch by composingFirstMethod("X-IG-Accept-Hint") {
instructions(TARGET_STRING())
}

View file

@ -0,0 +1,24 @@
package app.revanced.patches.instagram.hide.highlightsTray
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
val hideHighlightsTrayPatch = bytecodePatch(
name = "Hide highlights tray",
description = "Hides the highlights tray in profile section.",
use = false,
) {
compatibleWith("com.instagram.android")
apply {
highlightsUrlBuilderMethodMatch.method.apply {
val targetStringIndex = highlightsUrlBuilderMethodMatch[0]
val targetStringRegister = getInstruction<OneRegisterInstruction>(targetStringIndex).registerA
replaceInstruction(targetStringIndex, "const-string v$targetStringRegister, \"BOGUS\"")
}
}
}

View file

@ -17,10 +17,5 @@ internal val FEED_ITEM_KEYS_TO_BE_HIDDEN = arrayOf(
)
internal val BytecodePatchContext.feedItemParseFromJsonMethodMatch by composingFirstMethod("FeedItem") {
instructions(
predicates = unorderedAllOf(
predicates =
FEED_ITEM_KEYS_TO_BE_HIDDEN.map { it.invoke() }.toTypedArray(),
),
)
instructions(predicates = unorderedAllOf(predicates = FEED_ITEM_KEYS_TO_BE_HIDDEN.map { it() }.toTypedArray()))
}

View file

@ -6,7 +6,7 @@ import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
val hideSuggestedContentPatch = bytecodePatch(
val hideSuggestedContent = bytecodePatch(
name = "Hide suggested content",
description = "Hides suggested stories, reels, threads and survey from feed (Suggested posts will still be shown).",
use = false,

View file

@ -37,9 +37,9 @@ val openLinksExternallyPatch = bytecodePatch(
addInstructions(
urlResultObjIndex + 1,
"""
invoke-static { v$urlRegister }, $EXTENSION_CLASS_DESCRIPTOR->openExternally(Ljava/lang/String;)Z
move-result v$urlRegister
return v$urlRegister
invoke-static/range { v$urlRegister .. v$urlRegister }, $EXTENSION_CLASS_DESCRIPTOR->openExternally(Ljava/lang/String;)Z
move-result v0
return v0
""",
)
}

View file

@ -0,0 +1,15 @@
package app.revanced.patches.instagram.misc.removeBuildExpiredPopup
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.patch.BytecodePatchContext
internal const val MILLISECOND_IN_A_DAY_LITERAL = 0x5265c00L
internal val BytecodePatchContext.appUpdateLockoutBuilderMethod by gettingFirstMutableMethodDeclaratively(
"android.hardware.sensor.hinge_angle",
) {
instructions(MILLISECOND_IN_A_DAY_LITERAL())
}

View file

@ -0,0 +1,26 @@
package app.revanced.patches.instagram.misc.removeBuildExpiredPopup
import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.instructions
import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
@Suppress("unused")
val removeBuildExpiredPopupPatch = bytecodePatch(
name = "Remove build expired popup",
description = "Removes the popup that appears after a while, when the app version ages.",
) {
compatibleWith("com.instagram.android")
apply {
appUpdateLockoutBuilderMethod.apply {
val longToIntIndex = instructions.first { it.opcode == Opcode.LONG_TO_INT }.location.index
val appAgeRegister = getInstruction<TwoRegisterInstruction>(longToIntIndex).registerA
// Set app age to 0 days old such that the build expired popup doesn't appear.
addInstruction(longToIntIndex + 1, "const v$appAgeRegister, 0x0")
}
}
}

View file

@ -20,9 +20,7 @@ internal val BytecodePatchContext.storyUrlResponseJsonParserMethodMatch by compo
instructions("story_item_to_share_url"())
}
internal val BytecodePatchContext.profileUrlResponseJsonParserMethodMatch by composingFirstMethod(
"ProfileUrlResponse",
) {
internal val BytecodePatchContext.profileUrlResponseJsonParserMethodMatch by composingFirstMethod {
name("parseFromJson")
instructions("profile_to_share_url"())
}

View file

@ -10,7 +10,7 @@ val disableReelsScrollingPatch = bytecodePatch(
name = "Disable Reels scrolling",
description = "Disables the endless scrolling behavior in Instagram Reels, preventing swiping to the next Reel. " +
"Note: On a clean install, the 'Tip' animation may appear but will stop on its own after a few seconds.",
use = true,
use = false
) {
compatibleWith("com.instagram.android")

View file

@ -0,0 +1,11 @@
package app.revanced.patches.kleinanzeigen.ads
import app.revanced.patcher.definingClass
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.name
import app.revanced.patcher.patch.BytecodePatchContext
internal val BytecodePatchContext.getLibertyInitMethod by gettingFirstMutableMethodDeclaratively {
name("init")
definingClass { endsWith("/Liberty;") }
}

View file

@ -0,0 +1,16 @@
package app.revanced.patches.kleinanzeigen.ads
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val hideAdsPatch = bytecodePatch(
name = "Hide ads",
description = "Hides sponsored ads and Google Ads. Also happens to disable Microsoft Clarity analytics.",
) {
compatibleWith("com.ebay.kleinanzeigen")
apply {
getLibertyInitMethod.returnEarly()
}
}

View file

@ -11,7 +11,7 @@ val hideAdsPatch = bytecodePatch("Hide ads") {
apply {
admobHelperSetShowAdsMethod.addInstruction(0, "const p1, 0x0")
listOf(admobHelperShouldShowAdsMethod, filmFragmentShowAdsMethod, memberExtensionShowAdsMethod).forEach {
it.returnEarly(false)
it.returnEarly()
}
}
}

View file

@ -0,0 +1,14 @@
package app.revanced.patches.music.layout.hide.general
import app.revanced.patches.music.misc.litho.filter.lithoFilterPatch
import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.patches.shared.layout.hide.general.hideLayoutComponentsPatch
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
val hideLayoutComponentsPatch = hideLayoutComponentsPatch(
lithoFilterPatch = lithoFilterPatch,
getAddLithoFilter = { addLithoFilter },
settingsPatch = settingsPatch,
filterClasses = setOf("Lapp/revanced/extension/shared/patches/components/CustomFilter;"),
compatibleWithPackages = arrayOf("com.google.android.apps.youtube.music" to setOf("7.29.52", "8.10.52")),
)

View file

@ -5,24 +5,10 @@ import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.misc.settings.settingsPatch
import app.revanced.util.returnEarly
@Deprecated("This patch is useless by itself and has been merged into another patch.", ReplaceWith("unlockAndroidAutoMediaBrowserPatch"))
@Suppress("unused")
val bypassCertificateChecksPatch = bytecodePatch(
name = "Bypass certificate checks",
description = "Bypasses certificate checks which prevent YouTube Music from working on Android Auto.",
) {
dependsOn(
sharedExtensionPatch,
settingsPatch,
)
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52",
"8.10.52",
),
)
apply {
checkCertificateMethod.returnEarly(true)
}
dependsOn(unlockAndroidAutoMediaBrowserPatch)
}

View file

@ -1,11 +1,13 @@
package app.revanced.patches.music.misc.androidauto
import app.revanced.patcher.firstMutableMethodDeclaratively
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType
import com.android.tools.smali.dexlib2.iface.ClassDef
internal val BytecodePatchContext.checkCertificateMethod by gettingFirstMutableMethodDeclaratively(
"X509",
@ -14,3 +16,14 @@ internal val BytecodePatchContext.checkCertificateMethod by gettingFirstMutableM
parameterTypes("Ljava/lang/String;")
instructions("Failed to get certificate"(String::contains))
}
internal val BytecodePatchContext.searchMediaItemsConstructorMethod by gettingFirstMutableMethodDeclaratively(
"ytm_media_browser/search_media_items",
) {
returnType("V")
}
context(_: BytecodePatchContext)
internal fun ClassDef.getSearchMediaItemsExecuteMethod() = firstMutableMethodDeclaratively {
parameterTypes()
}

View file

@ -0,0 +1,36 @@
package app.revanced.patches.music.misc.androidauto
import app.revanced.patcher.extensions.fieldReference
import app.revanced.patcher.extensions.instructions
import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.registersUsed
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.Opcode
@Suppress("unused")
val unlockAndroidAutoMediaBrowserPatch = bytecodePatch(
name = "Unlock Android Auto Media Browser",
description = "Unlocks Android Auto Media Browser which enables the search function including speech to text.",
) {
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52",
"8.10.52",
),
)
apply {
checkCertificateMethod.returnEarly(true)
searchMediaItemsConstructorMethod.immutableClassDef.getSearchMediaItemsExecuteMethod().apply {
val targetIndex = instructions.indexOfFirst {
it.opcode == Opcode.IGET_OBJECT && it.fieldReference?.type == "Ljava/lang/String;"
}
val register = instructions[targetIndex].registersUsed.first()
replaceInstruction(targetIndex, "const-string v$register, \"com.google.android.apps.youtube.music\"")
}
}
}

View file

@ -7,20 +7,15 @@ import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch
@Suppress("unused")
val enableDebuggingPatch = enableDebuggingPatch(
block = {
dependsOn(
sharedExtensionPatch,
settingsPatch,
sharedExtensionPatch = sharedExtensionPatch,
settingsPatch = settingsPatch,
compatibleWithPackages = arrayOf(
"com.google.android.apps.youtube.music" to setOf(
"7.29.52",
"8.10.52"
)
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52",
"8.10.52"
)
)
},
),
// String feature flag does not appear to be present with YT Music.
hookStringFeatureFlag = false,
preferenceScreen = PreferenceScreen.MISC
preferenceScreen = PreferenceScreen.MISC,
)

View file

@ -0,0 +1,18 @@
package app.revanced.patches.music.misc.litho.filter
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.shared.conversionContextToStringMethod
import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
val lithoFilterPatch = lithoFilterPatch(
componentCreateInsertionIndex = {
// No supported version clobbers p2 so we can just do our things before the return instruction.
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
},
getConversionContextToStringMethod = BytecodePatchContext::conversionContextToStringMethod::get,
) {
dependsOn(sharedExtensionPatch)
}

View file

@ -1,7 +1,10 @@
package app.revanced.patches.music.shared
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.definingClass
import app.revanced.patcher.gettingFirstMethodDeclaratively
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.name
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
@ -16,3 +19,16 @@ internal val BytecodePatchContext.mainActivityOnCreateMethod by gettingFirstMuta
returnType("V")
parameterTypes("Landroid/os/Bundle;")
}
internal val BytecodePatchContext.conversionContextToStringMethod by gettingFirstMethodDeclaratively(
"ConversionContext{containerInternal=",
", gridColumnCount=",
", gridColumnIndex=",
", templateLoggerFactory=",
", rootDisposableContainer=",
", elementId=",
", identifierProperty=",
) {
name("toString")
parameterTypes()
}

View file

@ -0,0 +1,14 @@
package app.revanced.patches.nothingx.misc.extension
import app.revanced.patcher.definingClass
import app.revanced.patcher.name
import app.revanced.patches.shared.misc.extension.extensionHook
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch(
extensionName = "nothingx",
extensionHook {
name("onCreate")
definingClass { contains("BaseApplication") }
},
)

View file

@ -0,0 +1,19 @@
package app.revanced.patches.nothingx.misc.logk1token
import app.revanced.patcher.definingClass
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.name
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType
/**
* Fingerprint for the Application onCreate method.
* This is used to trigger scanning for existing log files on app startup.
*/
internal val BytecodePatchContext.applicationOnCreateMethod by gettingFirstMutableMethodDeclaratively {
name("onCreate")
definingClass { endsWith("BaseApplication;") }
returnType("V")
parameterTypes()
}

View file

@ -0,0 +1,31 @@
package app.revanced.patches.nothingx.misc.logk1token
import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.nothingx.misc.extension.sharedExtensionPatch
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/nothingx/patches/ShowK1TokensPatch;"
@Suppress("unused")
val showK1TokensPatch = bytecodePatch(
name = "Show K1 token(s)",
description = "Shows the K1 authentication token(s) in a dialog and logs it to logcat " +
"for pairing with GadgetBridge without requiring root access. " +
"After installing this patch, pair your watch with the Nothing X app and " +
"use the token from the dialog or logcat.",
) {
dependsOn(sharedExtensionPatch)
compatibleWith("com.nothing.smartcenter"())
apply {
// Hook Application.onCreate to get K1 tokens from database and log files.
// This will find K1 tokens that were already written to log files.
// p0 is the Application context in onCreate.
applicationOnCreateMethod.addInstruction(
0,
"invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->showK1Tokens(Landroid/content/Context;)V",
)
}
}

View file

@ -0,0 +1,58 @@
package app.revanced.patches.shared.layout.hide.general
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.music.misc.settings.PreferenceScreen
import app.revanced.patches.shared.misc.settings.preference.InputType
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.shared.misc.settings.preference.TextPreference
import kotlin.collections.toTypedArray
internal fun hideLayoutComponentsPatch(
lithoFilterPatch: Patch,
getAddLithoFilter: () -> (String) -> Unit, // Temporal hack until YouTube can use the shared litho filter patch.
settingsPatch: Patch,
additionalDependencies: Set<Patch> = emptySet(),
filterClasses: Set<String>,
vararg compatibleWithPackages: Pair<String, Set<String>?>,
executeBlock: BytecodePatchContext.() -> Unit = {},
) = bytecodePatch(
name = "Hide layout components",
description = "Adds options to hide general layout components.",
) {
dependsOn(
lithoFilterPatch,
settingsPatch,
*additionalDependencies.toTypedArray(),
addResourcesPatch,
)
compatibleWith(packages = compatibleWithPackages)
apply {
addResources("shared", "layout.hide.general.hideLayoutComponentsPatch")
PreferenceScreen.GENERAL.addPreferences(
PreferenceScreenPreference(
key = "revanced_custom_filter_screen",
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_custom_filter"),
TextPreference("revanced_custom_filter_strings", inputType = InputType.TEXT_MULTI_LINE),
),
),
)
val addLithoFilter = getAddLithoFilter()
filterClasses.forEach { className ->
addLithoFilter(className)
}
executeBlock()
}
}

View file

@ -3,8 +3,7 @@ package app.revanced.patches.shared.misc.debugging
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.all.misc.resources.addResources
@ -22,16 +21,20 @@ private const val EXTENSION_CLASS_DESCRIPTOR =
* Patch shared with YouTube and YT Music.
*/
internal fun enableDebuggingPatch(
block: BytecodePatchBuilder.() -> Unit = {},
executeBlock: BytecodePatchContext.() -> Unit = {},
sharedExtensionPatch: Patch,
settingsPatch: Patch,
vararg compatibleWithPackages: Pair<String, Set<String>>,
hookStringFeatureFlag: Boolean,
preferenceScreen: BasePreferenceScreen.Screen,
additionalDebugPreferences: List<BasePreference> = emptyList(),
) = bytecodePatch(
name = "Enable debugging",
description = "Adds options for debugging and exporting ReVanced logs to the clipboard.",
) {
compatibleWith(packages = compatibleWithPackages)
dependsOn(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
resourcePatch {
apply {
@ -54,38 +57,28 @@ internal fun enableDebuggingPatch(
},
)
block()
apply {
executeBlock()
addResources("shared", "misc.debugging.enableDebuggingPatch")
val preferences = mutableSetOf<BasePreference>(
val preferences = setOf(
SwitchPreference("revanced_debug"),
)
preferences.addAll(additionalDebugPreferences)
preferences.addAll(
listOf(
SwitchPreference("revanced_debug_stacktrace"),
SwitchPreference("revanced_debug_toast_on_error"),
NonInteractivePreference(
"revanced_debug_export_logs_to_clipboard",
tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference",
selectable = true,
),
NonInteractivePreference(
"revanced_debug_logs_clear_buffer",
tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference",
selectable = true,
),
NonInteractivePreference(
"revanced_debug_feature_flags_manager",
tag = "app.revanced.extension.shared.settings.preference.FeatureFlagsManagerPreference",
selectable = true,
),
SwitchPreference("revanced_debug_protobuffer"),
SwitchPreference("revanced_debug_stacktrace"),
SwitchPreference("revanced_debug_toast_on_error"),
NonInteractivePreference(
"revanced_debug_export_logs_to_clipboard",
tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference",
selectable = true,
),
NonInteractivePreference(
"revanced_debug_logs_clear_buffer",
tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference",
selectable = true,
),
NonInteractivePreference(
"revanced_debug_feature_flags_manager",
tag = "app.revanced.extension.shared.settings.preference.FeatureFlagsManagerPreference",
selectable = true,
),
)

View file

@ -0,0 +1,59 @@
package app.revanced.patches.shared.misc.litho.filter
import app.revanced.patcher.accessFlags
import app.revanced.patcher.composingFirstMethod
import app.revanced.patcher.custom
import app.revanced.patcher.definingClass
import app.revanced.patcher.gettingFirstMethod
import app.revanced.patcher.gettingFirstMethodDeclaratively
import app.revanced.patcher.gettingFirstMutableMethod
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.instructions
import app.revanced.patcher.invoke
import app.revanced.patcher.opcodes
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType
import app.revanced.patcher.strings
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val BytecodePatchContext.lithoFilterMethod by gettingFirstMutableMethodDeclaratively {
definingClass { endsWith("/LithoFilterPatch;") }
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
}
/**
* Matches a method that use the protobuf of our component.
*/
internal val BytecodePatchContext.protobufBufferReferenceMethod by gettingFirstMutableMethodDeclaratively {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
parameterTypes("I", "Ljava/nio/ByteBuffer;")
opcodes(Opcode.IPUT, Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT, Opcode.SUB_INT_2ADDR)
}
internal val BytecodePatchContext.componentContextParserMethodMatch by composingFirstMethod {
instructions("Number of bits must be positive"())
}
internal val BytecodePatchContext.emptyComponentMethod by gettingFirstMethodDeclaratively("EmptyComponent") {
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
parameterTypes()
custom {
immutableClassDef.methods.filter { AccessFlags.STATIC.isSet(it.accessFlags) }.size == 1
}
}
internal val BytecodePatchContext.componentCreateMethod by gettingFirstMutableMethod(
"Element missing correct type extension",
"Element missing type",
)
internal val BytecodePatchContext.lithoThreadExecutorMethod by gettingFirstMutableMethodDeclaratively {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameterTypes("I", "I", "I")
instructions(1L()) // 1L = default thread timeout.
custom { immutableClassDef.superclass == "Ljava/util/concurrent/ThreadPoolExecutor;" }
}

View file

@ -0,0 +1,210 @@
@file:Suppress("SpellCheckingInspection")
package app.revanced.patches.shared.misc.litho.filter
import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.removeInstructions
import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.firstClassDef
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.shared.conversionContextToStringMethod
import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findFreeRegister
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
/**
* Used to add a hook point to the extension stub.
*/
lateinit var addLithoFilter: (String) -> Unit
private set
/**
* Counts the number of filters added to the static field array.
*/
private var filterCount = 0
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/patches/litho/LithoFilterPatch;"
/**
* A patch that allows to filter Litho components based on their identifier or path.
*
* @param componentCreateInsertionIndex The index to insert the filtering code in the component create method.
* @param getConversionContextToStringMethod The Method of the conversion context to string method.
* @param executeBlock The additional execution block of the patch.
* @param block The additional block to build the patch.
*/
internal fun lithoFilterPatch(
componentCreateInsertionIndex: Method.() -> Int,
getConversionContextToStringMethod: BytecodePatchContext.() -> Method,
executeBlock: BytecodePatchContext.() -> Unit = {},
block: BytecodePatchBuilder.() -> Unit = {},
) = bytecodePatch(
description = "Hooks the method which parses the bytes into a ComponentContext to filter components.",
) {
dependsOn(
sharedExtensionPatch(),
)
/**
* The following patch inserts a hook into the method that parses the bytes into a ComponentContext.
* This method contains a StringBuilder object that represents the pathBuilder of the component.
* The pathBuilder is used to filter components by their path.
*
* Additionally, the method contains a reference to the component's identifier.
* The identifier is used to filter components by their identifier.
*
* The protobuf buffer is passed along from a different injection point before the filtering occurs.
* The buffer is a large byte array that represents the component tree.
* This byte array is searched for strings that indicate the current component.
*
* All modifications done here must allow all the original code to still execute
* even when filtering, otherwise memory leaks or poor app performance may occur.
*
* The following pseudocode shows how this patch works:
*
* class SomeOtherClass {
* // Called before ComponentContextParser.parseComponent() method.
* public void someOtherMethod(ByteBuffer byteBuffer) {
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
* ...
* }
* }
*
* class CreateComponentClass {
* public Component createComponent() {
* ...
*
* if (extensionClass.shouldFilter(identifier, path)) { // Inserted by this patch.
* return emptyComponent;
* }
* return originalUnpatchedComponent; // Original code.
* }
* }
*/
apply {
// Remove dummy filter from extenion static field
// and add the filters included during patching.
lithoFilterMethod.apply {
removeInstructions(2, 4) // Remove dummy filter.
addLithoFilter = { classDescriptor ->
addInstructions(
2,
"""
new-instance v1, $classDescriptor
invoke-direct { v1 }, $classDescriptor-><init>()V
const/16 v2, ${filterCount++}
aput-object v1, v0, v2
""",
)
}
}
// Add an interceptor to steal the protobuf of our component.
protobufBufferReferenceMethod.addInstruction(
0,
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
)
// Hook the method that parses bytes into a ComponentContext.
// Allow the method to run to completion, and override the
// return value with an empty component if it should be filtered.
// It is important to allow the original code to always run to completion,
// otherwise high memory usage and poor app performance can occur.
// Find the identifier/path fields of the conversion context.
val conversionContextIdentifierField = componentContextParserMethodMatch.let {
// Identifier field is loaded just before the string declaration.
val index = it.method.indexOfFirstInstructionReversedOrThrow(it[0]) {
// Our instruction reads a String from a field of the ConversionContext class.
val reference = getReference<FieldReference>()
reference?.definingClass == conversionContextToStringMethod.immutableClassDef.type &&
reference.type == "Ljava/lang/String;"
}
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()!!
}
val conversionContextPathBuilderField = conversionContextToStringMethod.immutableClassDef
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
// Find class and methods to create an empty component.
val builderMethodDescriptor = emptyComponentMethod.immutableClassDef.methods.single {
// The only static method in the class.
method ->
AccessFlags.STATIC.isSet(method.accessFlags)
}
val emptyComponentField = firstClassDef {
// Only one field that matches.
type == builderMethodDescriptor.returnType
}.fields.single()
// Match all component creations methods
componentCreateMethod.apply {
val insertIndex = componentCreateInsertionIndex()
val freeRegister = findFreeRegister(insertIndex)
val identifierRegister = findFreeRegister(insertIndex, freeRegister)
val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister)
addInstructionsAtControlFlowLabel(
insertIndex,
"""
move-object/from16 v$freeRegister, p2 # ConversionContext parameter
check-cast v$freeRegister, ${getConversionContextToStringMethod().immutableClassDef.type} # Check we got the actual ConversionContext
# Get identifier and path from ConversionContext
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
# Check if the component should be filtered.
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
move-result v$freeRegister
if-eqz v$freeRegister, :unfiltered
# Return an empty component
move-object/from16 v$freeRegister, p1
invoke-static { v$freeRegister }, $builderMethodDescriptor
move-result-object v$freeRegister
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
return-object v$freeRegister
:unfiltered
nop
""",
)
}
// TODO: Check if needed in music.
// Change Litho thread executor to 1 thread to fix layout issue in unpatched YouTube.
lithoThreadExecutorMethod.addInstructions(
0,
"""
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->getExecutorCorePoolSize(I)I
move-result p1
invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->getExecutorMaxThreads(I)I
move-result p2
""",
)
executeBlock()
}
afterDependents {
// Save the number of filters added.
lithoFilterMethod.replaceInstruction(0, "const/16 v0, $filterCount")
}
block()
}

View file

@ -0,0 +1,184 @@
package app.revanced.patches.strava.distractions
import app.revanced.com.android.tools.smali.dexlib2.iface.value.MutableBooleanEncodedValue.Companion.toMutable
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableClassDef
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.firstMutableClassDef
import app.revanced.patcher.firstMutableMethod
import app.revanced.patcher.patch.booleanOption
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.strava.misc.extension.sharedExtensionPatch
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference
import com.android.tools.smali.dexlib2.immutable.value.ImmutableBooleanEncodedValue
import com.android.tools.smali.dexlib2.util.MethodUtil
import java.util.logging.Logger
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/HideDistractionsPatch;"
private const val MODULAR_FRAMEWORK_CLASS_DESCRIPTOR_PREFIX = "Lcom/strava/modularframework"
private const val METHOD_SUFFIX = "\$original"
private data class FilterablePropertyFingerprint(
val name: String,
val parameterTypes: List<String> = listOf(),
)
private val fingerprints = arrayOf(
FilterablePropertyFingerprint("ChildrenEntries"),
FilterablePropertyFingerprint("Entries"),
FilterablePropertyFingerprint("Field", listOf("Ljava/lang/String;")),
FilterablePropertyFingerprint("Fields"),
FilterablePropertyFingerprint("MenuItems"),
FilterablePropertyFingerprint("Modules"),
FilterablePropertyFingerprint("Properties"),
FilterablePropertyFingerprint("StateMap"),
FilterablePropertyFingerprint("Submodules"),
)
@Suppress("unused")
val hideDistractionsPatch = bytecodePatch(
name = "Hide distractions",
description = "Hides elements that are not essential.",
) {
compatibleWith("com.strava")
dependsOn(sharedExtensionPatch)
val logger = Logger.getLogger(this::class.java.name)
val options = mapOf(
"upselling" to booleanOption(
name = "Upselling",
description = "Elements that suggest you subscribe.",
default = true,
required = true,
),
"promo" to booleanOption(
name = "Promotions",
default = true,
required = true,
),
"followSuggestions" to booleanOption(
name = "Who to Follow",
description = "Popular athletes, followers, people near you etc.",
default = true,
required = true,
),
"challengeSuggestions" to booleanOption(
name = "Suggested Challenges",
description = "Random challenges Strava wants you to join.",
default = true,
required = true,
),
"joinChallenge" to booleanOption(
name = "Join Challenge",
description = "Challenges your follows have joined.",
default = false,
required = true,
),
"joinClub" to booleanOption(
name = "Joined a club",
description = "Clubs your follows have joined.",
default = false,
required = true,
),
"activityLookback" to booleanOption(
name = "Your activity from X years ago",
default = false,
required = true,
),
)
apply {
// region Write option values into extension class.
val extensionClass = firstMutableClassDef { type == EXTENSION_CLASS_DESCRIPTOR }.apply {
options.forEach { (key, option) ->
staticFields.first { field -> field.name == key }.initialValue =
ImmutableBooleanEncodedValue.forBoolean(option.value == true).toMutable()
}
}
// endregion
// region Intercept all classes' property getter calls.
fun MutableMethod.cloneAndIntercept(
classDef: MutableClassDef,
extensionMethodName: String,
extensionMethodParameterTypes: List<String>,
) {
val extensionMethodReference = ImmutableMethodReference(
EXTENSION_CLASS_DESCRIPTOR,
extensionMethodName,
extensionMethodParameterTypes,
returnType,
)
if (extensionClass.directMethods.none { method ->
MethodUtil.methodSignaturesMatch(method, extensionMethodReference)
}
) {
logger.info { "Skipped interception of $this due to missing $extensionMethodReference" }
return
}
classDef.virtualMethods -= this
val clone = ImmutableMethod.of(this).toMutable()
classDef.virtualMethods += clone
if (implementation != null) {
val registers = List(extensionMethodParameterTypes.size) { index -> "p$index" }.joinToString(
separator = ",",
prefix = "{",
postfix = "}",
)
clone.addInstructions(
0,
"""
invoke-static $registers, $extensionMethodReference
move-result-object v0
return-object v0
""",
)
logger.fine { "Intercepted $this with $extensionMethodReference" }
}
name += METHOD_SUFFIX
classDef.virtualMethods += this
}
classDefs.filter { it.type.startsWith(MODULAR_FRAMEWORK_CLASS_DESCRIPTOR_PREFIX) }.forEach { classDef ->
val mutableClassDef by lazy { classDefs.getOrReplaceMutable(classDef) }
classDef.virtualMethods.forEach { method ->
fingerprints.find { fingerprint ->
method.name == "get${fingerprint.name}" && method.parameterTypes == fingerprint.parameterTypes
}?.let { fingerprint ->
// Upcast to the interface if this is an interface implementation.
val parameterType = classDef.interfaces.find {
classDefs.find { interfaceDef -> interfaceDef.type == it }?.virtualMethods?.any { interfaceMethod ->
MethodUtil.methodSignaturesMatch(interfaceMethod, method)
} == true
} ?: classDef.type
mutableClassDef.firstMutableMethod(method).cloneAndIntercept(
mutableClassDef,
"filter${fingerprint.name}",
listOf(parameterType) + fingerprint.parameterTypes,
)
}
}
}
// endregion
}
}

View file

@ -0,0 +1,202 @@
package app.revanced.patches.strava.groupkudos
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.instructions
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.util.childElementsSequence
import app.revanced.util.findElementByAttributeValueOrThrow
import app.revanced.util.getReference
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.builder.instruction.BuilderInstruction11x
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction31i
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
import org.w3c.dom.Element
private const val VIEW_CLASS_DESCRIPTOR = "Landroid/view/View;"
private const val ON_CLICK_LISTENER_CLASS_DESCRIPTOR = "Landroid/view/View\$OnClickListener;"
private var shakeToKudosStringId = -1
private var kudosIdId = -1
private var leaveIdId = -1
private val addGiveKudosButtonToLayoutPatch = resourcePatch {
fun String.toResourceId() = substring(2).toInt(16)
apply {
document("res/values/public.xml").use { public ->
fun Sequence<Element>.firstByName(name: String) = first {
it.getAttribute("name") == name
}
val publicElements = public.documentElement.childElementsSequence().filter {
it.tagName == "public"
}
val idElements = publicElements.filter {
it.getAttribute("type") == "id"
}
val stringElements = publicElements.filter {
it.getAttribute("type") == "string"
}
shakeToKudosStringId =
stringElements.firstByName("shake_to_kudos_dialog_title").getAttribute("id").toResourceId()
val kudosIdNode = idElements.firstByName("kudos").apply {
kudosIdId = getAttribute("id").toResourceId()
}
document("res/layout/grouped_activities_dialog_group_tab.xml").use { layout ->
layout.childNodes.findElementByAttributeValueOrThrow("android:id", "@id/leave_group_button_container")
.apply {
// Change from "FrameLayout".
layout.renameNode(this, namespaceURI, "LinearLayout")
val leaveButton = childElementsSequence().first()
// Get "Leave Group" button ID for bytecode matching.
val leaveButtonIdName = leaveButton.getAttribute("android:id").substringAfter('/')
leaveIdId = idElements.firstByName(leaveButtonIdName).getAttribute("id").toResourceId()
// Add surrounding padding to offset decrease on buttons.
setAttribute("android:paddingHorizontal", "@dimen/space_2xs")
// Place buttons next to each other with equal width.
val kudosButton = leaveButton.apply {
setAttribute("android:layout_width", "0dp")
setAttribute("android:layout_weight", "1")
// Decrease padding between buttons from "@dimen/button_large_padding" ...
setAttribute("android:paddingHorizontal", "@dimen/space_xs")
}.cloneNode(true) as Element
kudosButton.apply {
setAttribute("android:id", "@id/${kudosIdNode.getAttribute("name")}")
setAttribute("android:text", "@string/kudos_button")
}.let(::appendChild)
// Downgrade emphasis of "Leave Group" button from "primary".
leaveButton.setAttribute("app:emphasis", "secondary")
}
}
}
}
}
@Suppress("unused")
val addGiveGroupKudosButtonToGroupActivity = bytecodePatch(
name = "Add 'Give Kudos' button to 'Group Activity'",
description = "Adds a button that triggers the same action as shaking your phone would.",
) {
compatibleWith("com.strava")
dependsOn(addGiveKudosButtonToLayoutPatch)
apply {
val className = initMethod.immutableClassDef.type
val onClickListenerClassName = "${className.substringBeforeLast(';')}\$GiveKudosOnClickListener;"
initMethod.apply {
val constLeaveIdInstruction = instructions.filterIsInstance<BuilderInstruction31i>().first {
it.narrowLiteral == leaveIdId
}
val findViewByIdInstruction =
getInstruction<BuilderInstruction35c>(constLeaveIdInstruction.location.index + 1)
val moveViewInstruction = getInstruction<BuilderInstruction11x>(constLeaveIdInstruction.location.index + 2)
val checkCastButtonInstruction =
getInstruction<BuilderInstruction21c>(constLeaveIdInstruction.location.index + 3)
val buttonClassName = checkCastButtonInstruction.getReference<TypeReference>()!!.type
addInstructions(
constLeaveIdInstruction.location.index,
"""
${constLeaveIdInstruction.opcode.name} v${constLeaveIdInstruction.registerA}, $kudosIdId
${findViewByIdInstruction.opcode.name} { v${findViewByIdInstruction.registerC}, v${findViewByIdInstruction.registerD} }, ${findViewByIdInstruction.reference}
${moveViewInstruction.opcode.name} v${moveViewInstruction.registerA}
${checkCastButtonInstruction.opcode.name} v${checkCastButtonInstruction.registerA}, ${checkCastButtonInstruction.reference}
new-instance v0, $onClickListenerClassName
invoke-direct { v0, p0 }, $onClickListenerClassName-><init>($className)V
invoke-virtual { p3, v0 }, $buttonClassName->setOnClickListener($ON_CLICK_LISTENER_CLASS_DESCRIPTOR)V
""",
)
}
val actionHandlerMethod = initMethod.immutableClassDef.getActionHandlerMethod()
val constShakeToKudosStringIndex = actionHandlerMethod.instructions.indexOfFirst {
it is NarrowLiteralInstruction && it.narrowLiteral == shakeToKudosStringId
}
val getSingletonInstruction = actionHandlerMethod.instructions.filterIsInstance<BuilderInstruction21c>().last {
it.opcode == Opcode.SGET_OBJECT && it.location.index < constShakeToKudosStringIndex
}
val outerThisField = ImmutableField(
onClickListenerClassName,
"outerThis",
className,
PUBLIC.value or FINAL.value or SYNTHETIC.value,
null,
listOf(),
setOf(),
)
val initFieldMethod = ImmutableMethod(
onClickListenerClassName,
"<init>",
listOf(ImmutableMethodParameter(className, setOf(), "outerThis")),
"V",
PUBLIC.value or SYNTHETIC.value or CONSTRUCTOR.value,
setOf(),
setOf(),
MutableMethodImplementation(2),
).toMutable().apply {
addInstructions(
"""
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, $outerThisField
return-void
""",
)
}
val onClickMethod = ImmutableMethod(
onClickListenerClassName,
"onClick",
listOf(ImmutableMethodParameter(VIEW_CLASS_DESCRIPTOR, setOf(), "v")),
"V",
PUBLIC.value or FINAL.value,
setOf(),
setOf(),
MutableMethodImplementation(2),
).toMutable().apply {
addInstructions(
"""
sget-object p1, ${getSingletonInstruction.reference}
iget-object p0, p0, $outerThisField
invoke-virtual { p0, p1 }, $actionHandlerMethod
return-void
""",
)
}
ImmutableClassDef(
onClickListenerClassName,
PUBLIC.value or FINAL.value or SYNTHETIC.value,
"Ljava/lang/Object;",
listOf(ON_CLICK_LISTENER_CLASS_DESCRIPTOR),
"ProGuard", // Same as source file name of other classes.
listOf(),
setOf(outerThisField),
setOf(initFieldMethod, onClickMethod),
).let(classDefs::add)
}
}

View file

@ -0,0 +1,16 @@
package app.revanced.patches.strava.groupkudos
import app.revanced.patcher.firstMutableMethod
import app.revanced.patcher.gettingFirstMutableMethodDeclaratively
import app.revanced.patcher.name
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import com.android.tools.smali.dexlib2.iface.ClassDef
internal val BytecodePatchContext.initMethod by gettingFirstMutableMethodDeclaratively {
name("<init>")
parameterTypes("Lcom/strava/feed/view/modal/GroupTabFragment;", "Z", "Landroidx/fragment/app/FragmentManager;")
}
context(_: BytecodePatchContext)
internal fun ClassDef.getActionHandlerMethod() = firstMutableMethod("state")

View file

@ -0,0 +1,121 @@
package app.revanced.patches.strava.media.download
import app.revanced.patcher.extensions.ExternalLabel
import app.revanced.patcher.extensions.addInstruction
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.instructions
import app.revanced.patcher.firstClassDef
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.misc.mapping.ResourceType
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.strava.misc.extension.sharedExtensionPatch
import app.revanced.util.getReference
import app.revanced.util.writeRegister
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22c
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
private const val ACTION_CLASS_DESCRIPTOR = "Lcom/strava/bottomsheet/Action;"
private const val MEDIA_CLASS_DESCRIPTOR = "Lcom/strava/photos/data/Media;"
private const val MEDIA_DOWNLOAD_CLASS_DESCRIPTOR = "Lapp/revanced/extension/strava/AddMediaDownloadPatch;"
@Suppress("unused")
val addMediaDownloadPatch = bytecodePatch(
name = "Add media download",
description = "Extends the full-screen media viewer menu with items to copy or open their URLs or download them directly.",
) {
compatibleWith("com.strava")
dependsOn(
resourceMappingPatch,
sharedExtensionPatch,
)
apply {
val fragmentClass = firstClassDef { endsWith("/FullscreenMediaFragment;") }
// region Extend menu of `FullscreenMediaFragment` with actions.
fragmentClass.getCreateAndShowFragmentMethod().apply {
val setTrueIndex = instructions.indexOfFirst { instruction ->
instruction.opcode == Opcode.IPUT_BOOLEAN
}
val actionRegistrarRegister = getInstruction<BuilderInstruction22c>(setTrueIndex).registerB
val actionRegister = instructions.first { instruction ->
instruction.getReference<TypeReference>()?.type == ACTION_CLASS_DESCRIPTOR
}.writeRegister!!
fun addMenuItem(actionId: String, string: String, color: String, drawable: String) = addInstructions(
setTrueIndex + 1,
"""
new-instance v$actionRegister, $ACTION_CLASS_DESCRIPTOR
sget v${actionRegister + 1}, $MEDIA_DOWNLOAD_CLASS_DESCRIPTOR->$actionId:I
const v${actionRegister + 2}, 0x0
const v${actionRegister + 3}, ${ResourceType.STRING[string]}
const v${actionRegister + 4}, ${ResourceType.COLOR[color]}
const v${actionRegister + 5}, ${ResourceType.DRAWABLE[drawable]}
move/from16 v${actionRegister + 6}, v${actionRegister + 4}
invoke-direct/range { v$actionRegister .. v${actionRegister + 7} }, $ACTION_CLASS_DESCRIPTOR-><init>(ILjava/lang/String;IIIILjava/io/Serializable;)V
invoke-virtual { v$actionRegistrarRegister, v$actionRegister }, Lcom/strava/bottomsheet/a;->a(Lcom/strava/bottomsheet/BottomSheetItem;)V
""",
)
addMenuItem("ACTION_COPY_LINK", "copy_link", "core_o3", "actions_link_normal_xsmall")
addMenuItem(
"ACTION_OPEN_LINK",
"fallback_menu_item_open_in_browser",
"core_o3",
"actions_link_external_normal_xsmall",
)
addMenuItem("ACTION_DOWNLOAD", "download", "core_o3", "actions_download_normal_xsmall")
// Move media to last parameter of `Action` constructor.
val getMediaInstruction = instructions.first { instruction ->
instruction.getReference<FieldReference>()?.type == MEDIA_CLASS_DESCRIPTOR
}
addInstruction(
getMediaInstruction.location.index + 1,
"move-object/from16 v${actionRegister + 7}, v${getMediaInstruction.writeRegister}",
)
}
// endregion
// region Handle new actions.
val actionClass = firstClassDef {
type == ACTION_CLASS_DESCRIPTOR
}
val actionSerializableField = actionClass.instanceFields.first { field ->
field.type == "Ljava/io/Serializable;"
}
// Handle "copy link" & "open link" & "download" actions.
fragmentClass.getHandleMediaActionMethod().apply {
// Call handler if action ID < 0 (= custom).
val moveInstruction = instructions.first { instruction ->
instruction.opcode == Opcode.MOVE_RESULT
}
val indexAfterMoveInstruction = moveInstruction.location.index + 1
val actionIdRegister = moveInstruction.writeRegister
addInstructionsWithLabels(
indexAfterMoveInstruction,
"""
if-gez v$actionIdRegister, :move
check-cast p2, $ACTION_CLASS_DESCRIPTOR
iget-object v0, p2, $actionSerializableField
check-cast v0, $MEDIA_CLASS_DESCRIPTOR
invoke-static { v$actionIdRegister, v0 }, $MEDIA_DOWNLOAD_CLASS_DESCRIPTOR->handleAction(I$MEDIA_CLASS_DESCRIPTOR)Z
move-result v0
return v0
""",
ExternalLabel("move", instructions[indexAfterMoveInstruction]),
)
}
// endregion
}
}

View file

@ -0,0 +1,21 @@
package app.revanced.patches.strava.media.download
import app.revanced.patcher.accessFlags
import app.revanced.patcher.firstMutableMethodDeclaratively
import app.revanced.patcher.parameterTypes
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.returnType
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.ClassDef
context(_: BytecodePatchContext)
internal fun ClassDef.getCreateAndShowFragmentMethod() = firstMutableMethodDeclaratively("mediaType") {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returnType("V")
parameterTypes("L")
}
context(_: BytecodePatchContext)
internal fun ClassDef.getHandleMediaActionMethod() = firstMutableMethodDeclaratively {
parameterTypes("Landroid/view/View;", "Lcom/strava/bottomsheet/BottomSheetItem;")
}

View file

@ -1,4 +1,4 @@
package app.revanced.patches.strava.mediaupload
package app.revanced.patches.strava.media.upload
import app.revanced.patcher.definingClass
import app.revanced.patcher.firstMutableMethodDeclaratively

View file

@ -1,4 +1,4 @@
package app.revanced.patches.strava.mediaupload
package app.revanced.patches.strava.media.upload
import app.revanced.patcher.firstClassDef
import app.revanced.patcher.patch.bytecodePatch

View file

@ -0,0 +1,10 @@
package app.revanced.patches.strava.misc.extension
import app.revanced.patcher.definingClass
import app.revanced.patcher.name
import app.revanced.patches.shared.misc.extension.extensionHook
internal val applicationOnCreateHook = extensionHook {
name("onCreate")
definingClass { endsWith("/StravaApplication;") }
}

View file

@ -0,0 +1,5 @@
package app.revanced.patches.strava.misc.extension
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch("strava", applicationOnCreateHook)

View file

@ -1,63 +1,24 @@
package app.revanced.patches.strava.upselling
import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.removeInstruction
import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import app.revanced.patches.strava.distractions.hideDistractionsPatch
@Suppress("unused")
val disableSubscriptionSuggestionsPatch = bytecodePatch("Disable subscription suggestions") {
@Deprecated("Superseded by \"Hide distractions\" patch", ReplaceWith("hideDistractionsPatch"))
val disableSubscriptionSuggestionsPatch = bytecodePatch(
name = "Disable subscription suggestions",
) {
compatibleWith("com.strava")
apply {
val helperMethodName = "getModulesIfNotUpselling"
val pageSuffix = "_upsell"
val label = "original"
val className = getModulesMethodMatch.classDef.type
val immutableMethod = getModulesMethodMatch.method
val returnType = immutableMethod.returnType
getModulesMethodMatch.classDef.methods.add(
ImmutableMethod(
className,
helperMethodName,
emptyList(),
returnType,
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(3),
).toMutable().apply {
addInstructions(
"""
iget-object v0, p0, $className->page:Ljava/lang/String;
const-string v1, "$pageSuffix"
invoke-virtual {v0, v1}, Ljava/lang/String;->endsWith(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :$label
invoke-static {}, Ljava/util/Collections;->emptyList()Ljava/util/List;
move-result-object v0
return-object v0
:$label
iget-object v0, p0, $className->modules:Ljava/util/List;
return-object v0
""",
)
},
)
val getModulesIndex = getModulesMethodMatch[0]
immutableMethod.removeInstruction(getModulesIndex)
immutableMethod.addInstructions(
getModulesIndex,
"""
invoke-direct {p0}, $className->$helperMethodName()$returnType
move-result-object v0
""",
)
}
dependsOn(
hideDistractionsPatch.apply {
options["Upselling"] = true
options["Promotions"] = false
options["Who to Follow"] = false
options["Suggested Challenges"] = false
options["Join Challenge"] = false
options["Joined a club"] = false
options["Your activity from X years ago"] = false
},
)
}

View file

@ -3,10 +3,9 @@ package app.revanced.patches.youtube.layout.hide.general
import app.revanced.patcher.Match
import app.revanced.patcher.extensions.*
import app.revanced.patcher.immutableClassDef
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.layout.hide.general.hideLayoutComponentsPatch
import app.revanced.patches.shared.misc.mapping.ResourceType
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.settings.preference.*
@ -62,381 +61,363 @@ private const val DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME =
private const val COMMENTS_FILTER_CLASS_NAME =
"Lapp/revanced/extension/youtube/patches/components/CommentsFilter;"
private const val CUSTOM_FILTER_CLASS_NAME =
"Lapp/revanced/extension/youtube/patches/components/CustomFilter;"
"Lapp/revanced/extension/shared/patches/components/CustomFilter;"
private const val KEYWORD_FILTER_CLASS_NAME =
"Lapp/revanced/extension/youtube/patches/components/KeywordContentFilter;"
@Suppress("unused")
val hideLayoutComponentsPatch = bytecodePatch(
name = "Hide layout components",
description = "Adds options to hide general layout components.",
) {
dependsOn(
lithoFilterPatch,
settingsPatch,
addResourcesPatch,
val hideLayoutComponentsPatch = hideLayoutComponentsPatch(
lithoFilterPatch = lithoFilterPatch,
getAddLithoFilter = { addLithoFilter },
settingsPatch = settingsPatch,
additionalDependencies = setOf(
hideLayoutComponentsResourcePatch,
navigationBarHookPatch,
versionCheckPatch,
resourceMappingPatch,
)
compatibleWith(
"com.google.android.youtube"(
),
filterClasses = setOf(
LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR,
DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME,
COMMENTS_FILTER_CLASS_NAME,
KEYWORD_FILTER_CLASS_NAME,
CUSTOM_FILTER_CLASS_NAME,
),
compatibleWithPackages = arrayOf(
"com.google.android.youtube" to setOf(
"19.43.41",
"20.14.43",
"20.21.37",
"20.31.40",
),
),
) {
addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch")
PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_description_components_screen",
preferences = setOf(
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
SwitchPreference("revanced_hide_ask_section"),
SwitchPreference("revanced_hide_attributes_section"),
SwitchPreference("revanced_hide_chapters_section"),
SwitchPreference("revanced_hide_featured_links_section"),
SwitchPreference("revanced_hide_featured_videos_section"),
SwitchPreference("revanced_hide_info_cards_section"),
SwitchPreference("revanced_hide_how_this_was_made_section"),
SwitchPreference("revanced_hide_hype_points"),
SwitchPreference("revanced_hide_key_concepts_section"),
SwitchPreference("revanced_hide_podcast_section"),
SwitchPreference("revanced_hide_subscribe_button"),
SwitchPreference("revanced_hide_transcript_section"),
),
),
PreferenceScreenPreference(
"revanced_comments_screen",
preferences = setOf(
SwitchPreference("revanced_hide_comments_ai_chat_summary"),
SwitchPreference("revanced_hide_comments_ai_summary"),
SwitchPreference("revanced_hide_comments_channel_guidelines"),
SwitchPreference("revanced_hide_comments_by_members_header"),
SwitchPreference("revanced_hide_comments_section"),
SwitchPreference("revanced_hide_comments_community_guidelines"),
SwitchPreference("revanced_hide_comments_create_a_short_button"),
SwitchPreference("revanced_hide_comments_emoji_and_timestamp_buttons"),
SwitchPreference("revanced_hide_comments_preview_comment"),
SwitchPreference("revanced_hide_comments_thanks_button"),
),
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
),
SwitchPreference("revanced_hide_channel_bar"),
SwitchPreference("revanced_hide_channel_watermark"),
SwitchPreference("revanced_hide_crowdfunding_box"),
SwitchPreference("revanced_hide_emergency_box"),
SwitchPreference("revanced_hide_info_panels"),
SwitchPreference("revanced_hide_join_membership_button"),
SwitchPreference("revanced_hide_medical_panels"),
SwitchPreference("revanced_hide_quick_actions"),
SwitchPreference("revanced_hide_related_videos"),
SwitchPreference("revanced_hide_subscribers_community_guidelines"),
SwitchPreference("revanced_hide_timed_reactions"),
)
apply {
addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch")
PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_description_components_screen",
preferences = setOf(
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
SwitchPreference("revanced_hide_ask_section"),
SwitchPreference("revanced_hide_attributes_section"),
SwitchPreference("revanced_hide_chapters_section"),
SwitchPreference("revanced_hide_featured_links_section"),
SwitchPreference("revanced_hide_featured_videos_section"),
SwitchPreference("revanced_hide_info_cards_section"),
SwitchPreference("revanced_hide_how_this_was_made_section"),
SwitchPreference("revanced_hide_hype_points"),
SwitchPreference("revanced_hide_key_concepts_section"),
SwitchPreference("revanced_hide_podcast_section"),
SwitchPreference("revanced_hide_subscribe_button"),
SwitchPreference("revanced_hide_transcript_section"),
PreferenceScreen.FEED.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_keyword_content_screen",
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_hide_keyword_content_home"),
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
SwitchPreference("revanced_hide_keyword_content_search"),
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about",
tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference",
),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about_whole_words",
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
),
),
PreferenceScreenPreference(
"revanced_comments_screen",
preferences = setOf(
SwitchPreference("revanced_hide_comments_ai_chat_summary"),
SwitchPreference("revanced_hide_comments_ai_summary"),
SwitchPreference("revanced_hide_comments_channel_guidelines"),
SwitchPreference("revanced_hide_comments_by_members_header"),
SwitchPreference("revanced_hide_comments_section"),
SwitchPreference("revanced_hide_comments_community_guidelines"),
SwitchPreference("revanced_hide_comments_create_a_short_button"),
SwitchPreference("revanced_hide_comments_emoji_and_timestamp_buttons"),
SwitchPreference("revanced_hide_comments_preview_comment"),
SwitchPreference("revanced_hide_comments_thanks_button"),
),
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
),
PreferenceScreenPreference(
key = "revanced_hide_filter_bar_screen",
preferences = setOf(
SwitchPreference("revanced_hide_filter_bar_feed_in_feed"),
SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"),
SwitchPreference("revanced_hide_filter_bar_feed_in_search"),
SwitchPreference("revanced_hide_filter_bar_feed_in_history"),
),
SwitchPreference("revanced_hide_channel_bar"),
SwitchPreference("revanced_hide_channel_watermark"),
SwitchPreference("revanced_hide_crowdfunding_box"),
SwitchPreference("revanced_hide_emergency_box"),
SwitchPreference("revanced_hide_info_panels"),
SwitchPreference("revanced_hide_join_membership_button"),
SwitchPreference("revanced_hide_medical_panels"),
SwitchPreference("revanced_hide_quick_actions"),
SwitchPreference("revanced_hide_related_videos"),
SwitchPreference("revanced_hide_subscribers_community_guidelines"),
SwitchPreference("revanced_hide_timed_reactions"),
)
PreferenceScreen.FEED.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_keyword_content_screen",
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_hide_keyword_content_home"),
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
SwitchPreference("revanced_hide_keyword_content_search"),
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about",
tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference",
),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about_whole_words",
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
),
),
),
PreferenceScreenPreference(
key = "revanced_channel_screen",
preferences = setOf(
SwitchPreference("revanced_hide_community_button"),
SwitchPreference("revanced_hide_for_you_shelf"),
SwitchPreference("revanced_hide_join_button"),
SwitchPreference("revanced_hide_links_preview"),
SwitchPreference("revanced_hide_members_shelf"),
SwitchPreference("revanced_hide_store_button"),
SwitchPreference("revanced_hide_subscribe_button_in_channel_page"),
),
PreferenceScreenPreference(
key = "revanced_hide_filter_bar_screen",
preferences = setOf(
SwitchPreference("revanced_hide_filter_bar_feed_in_feed"),
SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"),
SwitchPreference("revanced_hide_filter_bar_feed_in_search"),
SwitchPreference("revanced_hide_filter_bar_feed_in_history"),
),
),
PreferenceScreenPreference(
key = "revanced_channel_screen",
preferences = setOf(
SwitchPreference("revanced_hide_community_button"),
SwitchPreference("revanced_hide_for_you_shelf"),
SwitchPreference("revanced_hide_join_button"),
SwitchPreference("revanced_hide_links_preview"),
SwitchPreference("revanced_hide_members_shelf"),
SwitchPreference("revanced_hide_store_button"),
SwitchPreference("revanced_hide_subscribe_button_in_channel_page"),
),
),
SwitchPreference("revanced_hide_album_cards"),
SwitchPreference("revanced_hide_artist_cards"),
SwitchPreference("revanced_hide_chips_shelf"),
SwitchPreference("revanced_hide_community_posts"),
SwitchPreference("revanced_hide_compact_banner"),
SwitchPreference("revanced_hide_expandable_card"),
SwitchPreference("revanced_hide_floating_microphone_button"),
SwitchPreference(
key = "revanced_hide_horizontal_shelves",
tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference",
),
SwitchPreference("revanced_hide_image_shelf"),
SwitchPreference("revanced_hide_latest_posts"),
SwitchPreference("revanced_hide_mix_playlists"),
SwitchPreference("revanced_hide_movies_section"),
SwitchPreference("revanced_hide_notify_me_button"),
SwitchPreference("revanced_hide_playables"),
SwitchPreference("revanced_hide_show_more_button"),
SwitchPreference("revanced_hide_surveys"),
SwitchPreference("revanced_hide_ticket_shelf"),
SwitchPreference("revanced_hide_upload_time"),
SwitchPreference("revanced_hide_video_recommendation_labels"),
SwitchPreference("revanced_hide_view_count"),
SwitchPreference("revanced_hide_visual_spacer"),
SwitchPreference("revanced_hide_doodles"),
)
),
SwitchPreference("revanced_hide_album_cards"),
SwitchPreference("revanced_hide_artist_cards"),
SwitchPreference("revanced_hide_chips_shelf"),
SwitchPreference("revanced_hide_community_posts"),
SwitchPreference("revanced_hide_compact_banner"),
SwitchPreference("revanced_hide_expandable_card"),
SwitchPreference("revanced_hide_floating_microphone_button"),
SwitchPreference(
key = "revanced_hide_horizontal_shelves",
tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference",
),
SwitchPreference("revanced_hide_image_shelf"),
SwitchPreference("revanced_hide_latest_posts"),
SwitchPreference("revanced_hide_mix_playlists"),
SwitchPreference("revanced_hide_movies_section"),
SwitchPreference("revanced_hide_notify_me_button"),
SwitchPreference("revanced_hide_playables"),
SwitchPreference("revanced_hide_show_more_button"),
SwitchPreference("revanced_hide_surveys"),
SwitchPreference("revanced_hide_ticket_shelf"),
SwitchPreference("revanced_hide_upload_time"),
SwitchPreference("revanced_hide_video_recommendation_labels"),
SwitchPreference("revanced_hide_view_count"),
SwitchPreference("revanced_hide_visual_spacer"),
SwitchPreference("revanced_hide_doodles"),
)
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
PreferenceScreenPreference(
key = "revanced_custom_filter_screen",
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_custom_filter"),
TextPreference("revanced_custom_filter_strings", inputType = InputType.TEXT_MULTI_LINE),
),
),
)
// region Mix playlists
addLithoFilter(LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR)
addLithoFilter(DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME)
addLithoFilter(COMMENTS_FILTER_CLASS_NAME)
addLithoFilter(KEYWORD_FILTER_CLASS_NAME)
addLithoFilter(CUSTOM_FILTER_CLASS_NAME)
parseElementFromBufferMethodMatch.let {
it.method.apply {
val startIndex = it[0]
val insertIndex = startIndex + 1
// region Mix playlists
val byteArrayParameter = "p3"
val conversionContextRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC }
val returnEmptyComponentRegister =
(returnEmptyComponentInstruction as FiveRegisterInstruction).registerC
val freeRegister =
findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister)
parseElementFromBufferMethodMatch.let {
it.method.apply {
val startIndex = it[0]
val insertIndex = startIndex + 1
val byteArrayParameter = "p3"
val conversionContextRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC }
val returnEmptyComponentRegister =
(returnEmptyComponentInstruction as FiveRegisterInstruction).registerC
val freeRegister =
findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister)
addInstructionsWithLabels(
insertIndex,
"""
invoke-static { v$conversionContextRegister, $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists(Ljava/lang/Object;[B)Z
move-result v$freeRegister
if-eqz v$freeRegister, :show
move-object v$returnEmptyComponentRegister, p1 # Required for 19.47
goto :return_empty_component
:show
nop
""",
ExternalLabel("return_empty_component", returnEmptyComponentInstruction),
)
}
}
// endregion
// region Watermark (legacy code for old versions of YouTube)
playerOverlayMethod.immutableClassDef.getShowWatermarkMethod().apply {
val index = implementation!!.instructions.size - 5
removeInstruction(index)
addInstructions(
index,
addInstructionsWithLabels(
insertIndex,
"""
invoke-static { v$conversionContextRegister, $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists(Ljava/lang/Object;[B)Z
move-result v$freeRegister
if-eqz v$freeRegister, :show
move-object v$returnEmptyComponentRegister, p1 # Required for 19.47
goto :return_empty_component
:show
nop
""",
ExternalLabel("return_empty_component", returnEmptyComponentInstruction),
)
}
}
// endregion
// region Watermark (legacy code for old versions of YouTube)
playerOverlayMethod.immutableClassDef.getShowWatermarkMethod().apply {
val index = implementation!!.instructions.size - 5
removeInstruction(index)
addInstructions(
index,
"""
invoke-static {}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->showWatermark()Z
move-result p2
""",
)
}
// endregion
// region Show more button
(if (is_20_26_or_greater) hideShowMoreButtonMethodMatch else hideShowMoreLegacyButtonMethodMatch).let {
it.method.apply {
val moveRegisterIndex = it[-1]
val viewRegister = getInstruction<OneRegisterInstruction>(moveRegisterIndex).registerA
val insertIndex = moveRegisterIndex + 1
addInstruction(
insertIndex,
"invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideShowMoreButton(Landroid/view/View;)V",
)
}
}
// endregion
// endregion
// region Show more button
// region crowdfunding box
crowdfundingBoxMethodMatch.let {
it.method.apply {
val insertIndex = it[-1]
val objectRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
(if (is_20_26_or_greater) hideShowMoreButtonMethodMatch else hideShowMoreLegacyButtonMethodMatch).let {
it.method.apply {
val moveRegisterIndex = it[-1]
val viewRegister = getInstruction<OneRegisterInstruction>(moveRegisterIndex).registerA
val insertIndex = moveRegisterIndex + 1
addInstruction(
insertIndex,
"invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideShowMoreButton(Landroid/view/View;)V",
)
}
addInstruction(
insertIndex,
"invoke-static {v$objectRegister}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideCrowdfundingBox(Landroid/view/View;)V",
)
}
}
// endregion
// endregion
// region crowdfunding box
crowdfundingBoxMethodMatch.let {
it.method.apply {
val insertIndex = it[-1]
val objectRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
// region hide album cards
addInstruction(
insertIndex,
"invoke-static {v$objectRegister}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideCrowdfundingBox(Landroid/view/View;)V",
)
}
albumCardsMethodMatch.let {
it.method.apply {
val checkCastAnchorIndex = it[-1]
val insertIndex = checkCastAnchorIndex + 1
val register = getInstruction<OneRegisterInstruction>(checkCastAnchorIndex).registerA
addInstruction(
insertIndex,
"invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideAlbumCard(Landroid/view/View;)V",
)
}
}
// endregion
// endregion
// region hide album cards
// region hide floating microphone
albumCardsMethodMatch.let {
it.method.apply {
val checkCastAnchorIndex = it[-1]
val insertIndex = checkCastAnchorIndex + 1
val register = getInstruction<OneRegisterInstruction>(checkCastAnchorIndex).registerA
showFloatingMicrophoneButtonMethodMatch.let {
it.method.apply {
val index = it[-1]
val register = getInstruction<TwoRegisterInstruction>(index).registerA
addInstruction(
insertIndex,
"invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideAlbumCard(Landroid/view/View;)V",
)
}
}
// endregion
// region hide floating microphone
showFloatingMicrophoneButtonMethodMatch.let {
it.method.apply {
val index = it[-1]
val register = getInstruction<TwoRegisterInstruction>(index).registerA
addInstructions(
index + 1,
"""
addInstructions(
index + 1,
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideFloatingMicrophoneButton(Z)Z
move-result v$register
""",
)
}
}
// endregion
// region 'Yoodles'
yoodlesImageViewMethod.apply {
findInstructionIndicesReversedOrThrow {
getReference<MethodReference>()?.name == "setImageDrawable"
}.forEach { insertIndex ->
val drawableRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerD
val imageViewRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerC
replaceInstruction(
insertIndex,
"invoke-static { v$imageViewRegister, v$drawableRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->" +
"setDoodleDrawable(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V",
)
}
}
// endregion
// region hide view count
hideViewCountMethodMatch.method.apply {
val startIndex = hideViewCountMethodMatch[0]
var returnStringRegister = getInstruction<OneRegisterInstruction>(startIndex).registerA
// Find the instruction where the text dimension is retrieved.
val applyDimensionIndex = indexOfFirstInstructionReversedOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_STATIC &&
reference?.definingClass == "Landroid/util/TypedValue;" &&
reference.returnType == "F" &&
reference.name == "applyDimension" &&
reference.parameterTypes == listOf("I", "F", "Landroid/util/DisplayMetrics;")
}
// A float value is passed which is used to determine subtitle text size.
val floatDimensionRegister = getInstruction<OneRegisterInstruction>(
applyDimensionIndex + 1,
).registerA
addInstructions(
applyDimensionIndex - 1,
"""
invoke-static { v$returnStringRegister, v$floatDimensionRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->modifyFeedSubtitleSpan(Landroid/text/SpannableString;F)Landroid/text/SpannableString;
move-result-object v$returnStringRegister
""",
)
}
}
// endregion
// endregion
// region hide filter bar
// region 'Yoodles'
/**
* Patch a [Method] with a given [instructions].
*
* @param RegisterInstruction The type of instruction to get the register from.
* @param insertIndexOffset The offset to add to the end index of the [Match.indices].
* @param hookRegisterOffset The offset to add to the register of the hook.
* @param instructions The instructions to add with the register as a parameter.
*/
fun <RegisterInstruction : OneRegisterInstruction> Match.patch(
insertIndexOffset: Int = 0,
hookRegisterOffset: Int = 0,
instructions: (Int) -> String,
) = method.apply {
val endIndex = get(-1)
val insertIndex = endIndex + insertIndexOffset
val register = getInstruction<RegisterInstruction>(endIndex + hookRegisterOffset).registerA
yoodlesImageViewMethod.apply {
findInstructionIndicesReversedOrThrow {
getReference<MethodReference>()?.name == "setImageDrawable"
}.forEach { insertIndex ->
val drawableRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerD
val imageViewRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerC
addInstructions(insertIndex, instructions(register))
}
filterBarHeightMethodMatch.patch<TwoRegisterInstruction> { register ->
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInFeed(I)I
move-result v$register
"""
}
searchResultsChipBarMethodMatch.patch<OneRegisterInstruction>(-1, -2) { register ->
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInSearch(I)I
move-result v$register
"""
}
relatedChipCloudMethodMatch.patch<OneRegisterInstruction>(1) { register ->
"invoke-static { v$register }, " +
"$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V"
replaceInstruction(
insertIndex,
"invoke-static { v$imageViewRegister, v$drawableRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->" +
"setDoodleDrawable(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V",
)
}
}
// endregion
// region hide view count
hideViewCountMethodMatch.method.apply {
val startIndex = hideViewCountMethodMatch[0]
var returnStringRegister = getInstruction<OneRegisterInstruction>(startIndex).registerA
// Find the instruction where the text dimension is retrieved.
val applyDimensionIndex = indexOfFirstInstructionReversedOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_STATIC &&
reference?.definingClass == "Landroid/util/TypedValue;" &&
reference.returnType == "F" &&
reference.name == "applyDimension" &&
reference.parameterTypes == listOf("I", "F", "Landroid/util/DisplayMetrics;")
}
// A float value is passed which is used to determine subtitle text size.
val floatDimensionRegister = getInstruction<OneRegisterInstruction>(
applyDimensionIndex + 1,
).registerA
addInstructions(
applyDimensionIndex - 1,
"""
invoke-static { v$returnStringRegister, v$floatDimensionRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->modifyFeedSubtitleSpan(Landroid/text/SpannableString;F)Landroid/text/SpannableString;
move-result-object v$returnStringRegister
""",
)
}
// endregion
// region hide filter bar
/**
* Patch a [Method] with a given [instructions].
*
* @param RegisterInstruction The type of instruction to get the register from.
* @param insertIndexOffset The offset to add to the end index of the [Match.indices].
* @param hookRegisterOffset The offset to add to the register of the hook.
* @param instructions The instructions to add with the register as a parameter.
*/
fun <RegisterInstruction : OneRegisterInstruction> Match.patch(
insertIndexOffset: Int = 0,
hookRegisterOffset: Int = 0,
instructions: (Int) -> String,
) = method.apply {
val endIndex = get(-1)
val insertIndex = endIndex + insertIndexOffset
val register = getInstruction<RegisterInstruction>(endIndex + hookRegisterOffset).registerA
addInstructions(insertIndex, instructions(register))
}
filterBarHeightMethodMatch.patch<TwoRegisterInstruction> { register ->
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInFeed(I)I
move-result v$register
"""
}
searchResultsChipBarMethodMatch.patch<OneRegisterInstruction>(-1, -2) { register ->
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInSearch(I)I
move-result v$register
"""
}
relatedChipCloudMethodMatch.patch<OneRegisterInstruction>(1) { register ->
"invoke-static { v$register }, " +
"$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V"
}
}

View file

@ -0,0 +1,13 @@
package app.revanced.patches.youtube.misc.audiofocus
import app.revanced.patcher.gettingFirstMutableMethod
import app.revanced.patcher.patch.BytecodePatchContext
internal val BytecodePatchContext.audioFocusChangeListenerMethod by gettingFirstMutableMethod(
"AudioFocus DUCK",
"AudioFocus loss; Will lower volume",
)
internal val BytecodePatchContext.audioFocusRequestBuilderMethod by gettingFirstMutableMethod(
"Can't build an AudioFocusRequestCompat instance without a listener",
)

View file

@ -0,0 +1,65 @@
package app.revanced.patches.youtube.misc.audiofocus
import app.revanced.patcher.extensions.ExternalLabel
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch;"
val pauseOnAudioInterruptPatch = bytecodePatch(
name = "Pause on audio interrupt",
description = "Adds an option to pause playback instead of lowering volume when other audio plays.",
) {
dependsOn(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
)
compatibleWith(
"com.google.android.youtube"(
"20.14.43",
),
)
apply {
addResources("youtube", "misc.audiofocus.pauseOnAudioInterruptPatch")
PreferenceScreen.MISC.addPreferences(
SwitchPreference("revanced_pause_on_audio_interrupt"),
)
// Hook the builder method that creates AudioFocusRequest.
// At the start, set the willPauseWhenDucked field (b) to true if setting is enabled.
val builderClass = audioFocusRequestBuilderMethod.definingClass
audioFocusRequestBuilderMethod.addInstructionsWithLabels(
0,
"""
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->shouldPauseOnAudioInterrupt()Z
move-result v0
if-eqz v0, :skip_override
const/4 v0, 0x1
iput-boolean v0, p0, $builderClass->b:Z
""",
ExternalLabel("skip_override", audioFocusRequestBuilderMethod.getInstruction(0)),
)
// Also hook the audio focus change listener as a backup.
audioFocusChangeListenerMethod.addInstructions(
0,
"""
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->overrideAudioFocusChange(I)I
move-result p1
""",
)
}
}

View file

@ -1,33 +1,22 @@
package app.revanced.patches.youtube.misc.debugging
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch;
import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
@Suppress("unused")
val enableDebuggingPatch = enableDebuggingPatch(
block = {
dependsOn(
sharedExtensionPatch,
settingsPatch,
sharedExtensionPatch = sharedExtensionPatch,
settingsPatch = settingsPatch,
compatibleWithPackages = arrayOf(
"com.google.android.youtube" to setOf(
"19.43.41",
"20.14.43",
"20.21.37",
"20.31.40",
)
compatibleWith(
"com.google.android.youtube"(
"19.43.41",
"20.14.43",
"20.21.37",
"20.31.40",
)
)
},
executeBlock = {
addResources("youtube", "misc.debugging.enableDebuggingPatch")
},
),
hookStringFeatureFlag = true,
preferenceScreen = PreferenceScreen.MISC,
additionalDebugPreferences = listOf(SwitchPreference("revanced_debug_protobuffer"))
)

View file

@ -1,40 +0,0 @@
package app.revanced.util
import java.io.File
/**
* Comments out the non-standard <app> and <patch> tags.
*
* Previously this was done on Crowdin after pushing.
* But Crowdin preprocessing has randomly failed but still used the unmodified
* strings.xml file, which effectively deletes all patch strings from Crowdin.
*/
internal fun main(args: Array<String>) {
if (args.size != 2) {
throw RuntimeException("Exactly two arguments are required: <input_file> <output_file>")
}
val inputFilePath = args[0]
val inputFile = File(inputFilePath)
if (!inputFile.exists()) {
throw RuntimeException(
"Input file not found: $inputFilePath currentDirectory: " + File(".").canonicalPath
)
}
// Comment out the non-standard tags. Otherwise Crowdin interprets the file
// not as Android but instead a generic xml file where strings are
// identified by xml position and not key.
val content = inputFile.readText()
val tagRegex = """((<app\s+.*>)|(</app>)|(<patch\s+.*>)|(</patch>))""".toRegex()
val modifiedContent = content.replace(tagRegex, """<!-- $1 -->""")
// Write modified content to the output file (creates file if it doesn't exist).
val outputFilePath = args[1]
val outputFile = File(outputFilePath)
outputFile.parentFile?.mkdirs()
outputFile.writeText(modifiedContent)
println("Preprocessed strings.xml to: $outputFilePath")
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -224,12 +224,14 @@ Second \"item\" text"</string>
<!-- Translations of this should not be longer than the original English text, otherwise the text can be clipped and not entirely shown. -->
</patch>
<patch id="misc.announcements.announcementsPatch">
<string name="revanced_announcements_dialog_dismiss">খাৰিজ কৰক</string>
<string name="youtube.misc.announcements.announcementsPatch.revanced_announcements_dialog_dismiss">খাৰিজ কৰক</string>
</patch>
<patch id="misc.loopvideo.loopVideoPatch">
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -21,60 +21,60 @@ Second \"item\" text"</string>
<resources>
<app id="shared">
<patch id="layout.branding.baseCustomBrandingPatch">
<string name="revanced_custom_branding_name_title">نام برنامه</string>
<string name="shared.layout.branding.baseCustomBrandingPatch.revanced_custom_branding_name_title">نام برنامه</string>
<!-- Translations of this should be identical to revanced_custom_branding_icon_entry_5 -->
<string name="revanced_custom_branding_icon_title">آیکون برنامه</string>
<string name="shared.layout.branding.baseCustomBrandingPatch.revanced_custom_branding_icon_title">آیکون برنامه</string>
<!-- Translation of this should be identical to revanced_header_logo_entry_5 -->
<!-- Translations of this should be identical to revanced_custom_branding_name_entry_5 -->
</patch>
<patch id="misc.checks.checkEnvironmentPatch">
<string name="revanced_check_environment_failed_title">بررسی ناموفق بود</string>
<string name="revanced_check_environment_dialog_open_official_source_button">رفتن به وبسایت رسمی</string>
<string name="revanced_check_environment_dialog_ignore_button">نادیده بگیر</string>
<string name="revanced_check_environment_failed_message">&lt;h5&gt;به نظر نمی‌رسد این برنامه توسط شما وصله شده باشد.&lt;/h5&gt;&lt;br&gt;این برنامه ممکن است به درستی کار نکند، &lt;b&gt;ممکن است استفاده از آن مضر یا حتی خطرناک باشد&lt;/b&gt;.&lt;br&gt;&lt;بر&gt;این برنامه از قبل دریافت شده است یا این چک از قبل دریافت شده است else:&lt;br&gt;&lt;br&gt;&lt;small&gt;%1$s&lt;/small&gt;&lt;br&gt;اکیداً توصیه می‌شود که &lt;b&gt;این برنامه را حذف نصب کنید و خودتان آن را وصله کنید&lt;/b&gt; برای اطمینان از اینکه از یک برنامه معتبر و ایمن استفاده می‌کنید.&lt;p&gt;&lt;br&gt;اگر نادیده گرفته شود، این هشدار فقط دو بار نشان داده می‌شود.</string>
<string name="revanced_check_environment_not_same_patching_device">روی دستگاه دیگری وصله شده است</string>
<string name="revanced_check_environment_manager_not_expected_installer">به وسیله ReVanced Manager نصب نشده است</string>
<string name="revanced_check_environment_not_near_patch_time">بیشتر از ۱۰ دقیقه پیش وصله شده است</string>
<string name="revanced_check_environment_not_near_patch_time_days">%s روز پیش وصله شده است</string>
<string name="revanced_check_environment_not_near_patch_time_invalid">تاریخ ایجاد APK مخدوش شده است</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_failed_title">بررسی ناموفق بود</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_dialog_open_official_source_button">رفتن به وبسایت رسمی</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_dialog_ignore_button">نادیده بگیر</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_failed_message">&lt;h5&gt;به نظر نمی‌رسد این برنامه توسط شما وصله شده باشد.&lt;/h5&gt;&lt;br&gt;این برنامه ممکن است به درستی کار نکند، &lt;b&gt;ممکن است استفاده از آن مضر یا حتی خطرناک باشد&lt;/b&gt;.&lt;br&gt;&lt;بر&gt;این برنامه از قبل دریافت شده است یا این چک از قبل دریافت شده است else:&lt;br&gt;&lt;br&gt;&lt;small&gt;%1$s&lt;/small&gt;&lt;br&gt;اکیداً توصیه می‌شود که &lt;b&gt;این برنامه را حذف نصب کنید و خودتان آن را وصله کنید&lt;/b&gt; برای اطمینان از اینکه از یک برنامه معتبر و ایمن استفاده می‌کنید.&lt;p&gt;&lt;br&gt;اگر نادیده گرفته شود، این هشدار فقط دو بار نشان داده می‌شود.</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_same_patching_device">روی دستگاه دیگری وصله شده است</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_manager_not_expected_installer">به وسیله ReVanced Manager نصب نشده است</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time">بیشتر از ۱۰ دقیقه پیش وصله شده است</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time_days">%s روز پیش وصله شده است</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time_invalid">تاریخ ایجاد APK مخدوش شده است</string>
</patch>
<patch id="misc.dns.checkWatchHistoryDomainNameResolutionPatch">
</patch>
<patch id="misc.settings.settingsResourcePatch">
<string name="revanced_settings_submenu_title">تنظیمات</string>
<string name="revanced_settings_reset">بازنشانی</string>
<string name="revanced_settings_restart">راه‌اندازی مجدد</string>
<string name="revanced_settings_import">واردکردن</string>
<string name="revanced_settings_import_copy">رونوشت‌</string>
<string name="revanced_settings_import_reset">بازگرداندن تنظیمات ReVanced به پیش‌فرض</string>
<string name="revanced_settings_import_success">%d تنظیمات وارد شدند</string>
<string name="revanced_settings_import_failure_parse">واردکردن انجام نشد: %s</string>
<string name="revanced_settings_search_hint">تنظیمات جستجو</string>
<string name="revanced_settings_search_no_results_title">نتایجی برای %s یافت نشد</string>
<string name="revanced_settings_search_no_results_summary">کلیدواژه دیگری را امتحان کنید</string>
<string name="revanced_settings_search_remove_message">حذف از تاریخچه جستجو؟</string>
<string name="revanced_settings_search_empty_history_title">تاریخچه جستجو خالی است</string>
<string name="revanced_settings_search_history_title">نمایش تاریخچه جستجوی تنظیمات</string>
<string name="revanced_show_menu_icons_title">نمایش آیکون تنظیمات ReVanced</string>
<string name="revanced_show_menu_icons_summary_on">نمادهای تنظیمات نشان داده می‌شوند</string>
<string name="revanced_show_menu_icons_summary_off">نمادهای تنظیمات نمایش داده نمی شوند</string>
<string name="revanced_language_title">زبان ReVanced</string>
<string name="revanced_language_DEFAULT">زبان برنامه</string>
<string name="revanced_pref_import_export_title">وارد کردن/صادر کردن</string>
<string name="revanced_pref_import_export_summary">وارد کردن / صادر کردن تنظیمات ReVanced</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_submenu_title">تنظیمات</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_reset">بازنشانی</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_restart">راه‌اندازی مجدد</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_import">واردکردن</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_import_copy">رونوشت‌</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_import_reset">بازگرداندن تنظیمات ReVanced به پیش‌فرض</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_import_success">%d تنظیمات وارد شدند</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_import_failure_parse">واردکردن انجام نشد: %s</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_search_hint">تنظیمات جستجو</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_search_no_results_title">نتایجی برای %s یافت نشد</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_search_no_results_summary">کلیدواژه دیگری را امتحان کنید</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_search_remove_message">حذف از تاریخچه جستجو؟</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_search_empty_history_title">تاریخچه جستجو خالی است</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_search_history_title">نمایش تاریخچه جستجوی تنظیمات</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_show_menu_icons_title">نمایش آیکون تنظیمات ReVanced</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_show_menu_icons_summary_on">نمادهای تنظیمات نشان داده می‌شوند</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_show_menu_icons_summary_off">نمادهای تنظیمات نمایش داده نمی شوند</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_language_title">زبان ReVanced</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_language_DEFAULT">زبان برنامه</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_pref_import_export_title">وارد کردن/صادر کردن</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_pref_import_export_summary">وارد کردن / صادر کردن تنظیمات ReVanced</string>
<!-- Settings about dialog. -->
<string name="revanced_settings_about_links_body">شما درحال استفاده از نسخه &lt;i&gt;%s&lt;/i&gt; از پچ Revanced هستید</string>
<string name="revanced_settings_about_links_dev_header">توجه</string>
<string name="revanced_settings_about_links_header">لینک‌های رسمی</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_about_links_body">شما درحال استفاده از نسخه &lt;i&gt;%s&lt;/i&gt; از پچ Revanced هستید</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_about_links_dev_header">توجه</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_about_links_header">لینک‌های رسمی</string>
<!-- NOTE: the about strings above are duplicated in the TikTok about screen code,
and changes made here must also be made there. -->
</patch>
<patch id="misc.gms.gmsCoreSupportResourcePatch">
<!-- Translations of this should not be longer than the original English text, otherwise the text can be clipped and not entirely shown. -->
<string name="gms_core_toast_not_installed_message">MicroG GmsCore نصب نشده است. آنرا نصب کنید.</string>
<string name="gms_core_dialog_title">اقدام لازم است</string>
<string name="gms_core_dialog_open_website_text">باز کردن تارنما</string>
<string name="gms_core_dialog_continue_text">ادامه</string>
<string name="shared.misc.gms.gmsCoreSupportResourcePatch.gms_core_toast_not_installed_message">MicroG GmsCore نصب نشده است. آنرا نصب کنید.</string>
<string name="shared.misc.gms.gmsCoreSupportResourcePatch.gms_core_dialog_title">اقدام لازم است</string>
<string name="shared.misc.gms.gmsCoreSupportResourcePatch.gms_core_dialog_open_website_text">باز کردن تارنما</string>
<string name="shared.misc.gms.gmsCoreSupportResourcePatch.gms_core_dialog_continue_text">ادامه</string>
</patch>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
</patch>
@ -82,22 +82,22 @@ Second \"item\" text"</string>
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
</patch>
<patch id="misc.debugging.enableDebuggingPatch">
<string name="revanced_debug_screen_title">عیب‌یابی</string>
<string name="revanced_debug_screen_summary">فعال یا غیرفعال کردن گزینه‌های عیب یابی</string>
<string name="revanced_debug_title">گزارش عیب</string>
<string name="revanced_debug_summary_on">لاگ عیب فعال است</string>
<string name="revanced_debug_summary_off">لاگ عیب غیرفعال است</string>
<string name="shared.misc.debugging.enableDebuggingPatch.revanced_debug_screen_title">عیب‌یابی</string>
<string name="shared.misc.debugging.enableDebuggingPatch.revanced_debug_screen_summary">فعال یا غیرفعال کردن گزینه‌های عیب یابی</string>
<string name="shared.misc.debugging.enableDebuggingPatch.revanced_debug_title">گزارش عیب</string>
<string name="shared.misc.debugging.enableDebuggingPatch.revanced_debug_summary_on">لاگ عیب فعال است</string>
<string name="shared.misc.debugging.enableDebuggingPatch.revanced_debug_summary_off">لاگ عیب غیرفعال است</string>
</patch>
<patch id="misc.privacy.sanitizeSharingLinksPatch">
</patch>
</app>
<app id="youtube">
<patch id="misc.settings.settingsPatch">
<string name="revanced_settings_screen_00_about_title">درباره</string>
<string name="revanced_settings_screen_04_general_title">عمومی</string>
<string name="revanced_settings_screen_05_player_title">اجراکننده</string>
<string name="revanced_settings_screen_07_seekbar_title">نوار جریان پخش</string>
<string name="revanced_settings_screen_12_video_title">ويدئو</string>
<string name="youtube.misc.settings.settingsPatch.revanced_settings_screen_00_about_title">درباره</string>
<string name="youtube.misc.settings.settingsPatch.revanced_settings_screen_04_general_title">عمومی</string>
<string name="youtube.misc.settings.settingsPatch.revanced_settings_screen_05_player_title">اجراکننده</string>
<string name="youtube.misc.settings.settingsPatch.revanced_settings_screen_07_seekbar_title">نوار جریان پخش</string>
<string name="youtube.misc.settings.settingsPatch.revanced_settings_screen_12_video_title">ويدئو</string>
</patch>
<patch id="misc.backgroundplayback.backgroundPlaybackPatch">
</patch>
@ -108,8 +108,8 @@ Second \"item\" text"</string>
This item appear in the Subscriptions feed for future livestreams or unreleased videos. -->
<!-- 'Show more' should be translated with the same localized wording that YouTube displays.
This button usually appears when searching for a YT creator. -->
<string name="revanced_hide_show_more_button_title">پنهان سازی دکمه \'نمایش بیشتر\'</string>
<string name="revanced_hide_ticket_shelf_title">پنهان سازی قفسه بلیط</string>
<string name="youtube.layout.hide.general.hideLayoutComponentsPatch.revanced_hide_show_more_button_title">پنهان سازی دکمه \'نمایش بیشتر\'</string>
<string name="youtube.layout.hide.general.hideLayoutComponentsPatch.revanced_hide_ticket_shelf_title">پنهان سازی قفسه بلیط</string>
<!-- 'People also watched' and 'You might also like' should be translated using the same localized wording YouTube displays. -->
<!-- https://logos.fandom.com/wiki/YouTube/Yoodles -->
<!-- 'Join' should be translated using the same localized wording YouTube displays.
@ -241,12 +241,12 @@ Second \"item\" text"</string>
</patch>
<patch id="layout.sponsorblock.sponsorBlockResourcePatch">
<!-- Translations should use language similar to 'revanced_ryd_compact_layout_title'. -->
<string name="revanced_sb_general">عمومی</string>
<string name="revanced_sb_settings_copy">رونوشت‌</string>
<string name="youtube.layout.sponsorblock.sponsorBlockResourcePatch.revanced_sb_general">عمومی</string>
<string name="youtube.layout.sponsorblock.sponsorBlockResourcePatch.revanced_sb_settings_copy">رونوشت‌</string>
<!-- Toast shown if network connection times out. Translations of this should not be longer than the original English or the text can be clipped and not entirely shown. -->
<!-- A segment start and end time, such as "02:10 to 03:40". -->
<!-- Shown in the settings preferences, and translations can be any text length. -->
<string name="revanced_sb_about_title">درباره</string>
<string name="youtube.layout.sponsorblock.sponsorBlockResourcePatch.revanced_sb_about_title">درباره</string>
</patch>
<patch id="layout.formfactor.changeFormFactorPatch">
</patch>
@ -283,6 +283,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">
@ -356,10 +358,10 @@ Second \"item\" text"</string>
<!-- Twitch specific internal debug mode, and not the same as 'revanced_debug_title'. -->
</patch>
<patch id="misc.settings.settingsPatch">
<string name="revanced_about_title">درباره</string>
<string name="revanced_twitch_debug_title">گزارش عیب</string>
<string name="revanced_twitch_debug_summary_on">لاگ عیب فعال است</string>
<string name="revanced_twitch_debug_summary_off">لاگ عیب غیرفعال است</string>
<string name="twitch.misc.settings.settingsPatch.revanced_about_title">درباره</string>
<string name="twitch.misc.settings.settingsPatch.revanced_twitch_debug_title">گزارش عیب</string>
<string name="twitch.misc.settings.settingsPatch.revanced_twitch_debug_summary_on">لاگ عیب فعال است</string>
<string name="twitch.misc.settings.settingsPatch.revanced_twitch_debug_summary_off">لاگ عیب غیرفعال است</string>
</patch>
</app>
</resources>

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -38,8 +38,7 @@ Second \"item\" text"</string>
<!-- Translations of this should not be longer than the original English text, otherwise the text can be clipped and not entirely shown. -->
</patch>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string name="revanced_spoof_video_streams_screen_summary">प्लेबैक समस्याओं को रोकने के लिए क्लाइंट वीडियो स्ट्रीम को स्पूफ करें</string>
<string name="revanced_spoof_video_streams_screen_summary">प्लेबैक समस्याओं को रोकने के लिए क्लाइंट वीडियो स्ट्रीम को स्पूफ करें</string>
<string name="shared.misc.fix.playback.spoofVideoStreamsPatch.revanced_spoof_video_streams_screen_summary">प्लेबैक समस्याओं को रोकने के लिए क्लाइंट वीडियो स्ट्रीम को स्पूफ करें</string>
</patch>
<patch id="misc.audio.forceOriginalAudioPatch">
<!-- 'Spoof video streams' should be the same translation used for 'revanced_spoof_video_streams_screen_title'. -->
@ -231,6 +230,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -26,12 +26,12 @@ Second \"item\" text"</string>
<!-- Translations of this should be identical to revanced_custom_branding_name_entry_5 -->
</patch>
<patch id="misc.checks.checkEnvironmentPatch">
<string name="revanced_check_environment_failed_title">Provjere nisu uspjele</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_failed_title">Provjere nisu uspjele</string>
</patch>
<patch id="misc.dns.checkWatchHistoryDomainNameResolutionPatch">
</patch>
<patch id="misc.settings.settingsResourcePatch">
<string name="revanced_settings_save">Sačuvaj</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_save">Sačuvaj</string>
<!-- Settings about dialog. -->
<!-- NOTE: the about strings above are duplicated in the TikTok about screen code,
and changes made here must also be made there. -->
@ -231,6 +231,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -26,25 +26,25 @@ Second \"item\" text"</string>
<!-- Translations of this should be identical to revanced_custom_branding_name_entry_5 -->
</patch>
<patch id="misc.checks.checkEnvironmentPatch">
<string name="revanced_check_environment_failed_title">ಪರಿಶೀಲನೆ ವಿಫಲವಾಗಿದೆ</string>
<string name="revanced_check_environment_dialog_open_official_source_button">ಅಧಿಕೃತ ಜಾಲತಾಣ ತೆರೆಯಿರಿ</string>
<string name="revanced_check_environment_dialog_ignore_button">ನಿರ್ಲಕ್ಷಿಸು</string>
<string name="revanced_check_environment_failed_message">&lt;h5&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ನೀವು ಪ್ಯಾಚ್ ಮಾಡಿದಂತೆ ಕಾಣುತ್ತಿಲ್ಲ.&lt;/h5&gt;&lt;br&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸದೇ ಇರಬಹುದು, &lt;b&gt; ಹಾಗು ಉಪಯೋಗಿಸಲು ಹಾನಿಕಾರಕ ಅಥವಾ ಅಪಾಯಕಾರಿಯಾಗಿರಬಹುದು&lt;/b&gt;.&lt;br&gt;&lt;br&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಮೊದಲೇ ಪ್ಯಾಚ್ ಆಗಿದೆ ಅಥವಾ ಬೇರೆಯವರಿಂದ ಪಡೆದದ್ದು ಎಂದು ಈ ಪರಿಶೀಲನೆಗಳು ಸೂಚಿಸುತ್ತವೆ:&lt;br&gt;&lt;br&gt;&lt;small&gt;%1$s&lt;/small&gt;&lt;br&gt;ನೀವು ದೃಢೀಕೃತ ಮತ್ತು ಸುರಕ್ಷಿತ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಬಳಸುತ್ತಿರುವಿರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಲು &lt;b&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಿ ಮತ್ತು ನೀವೇ ಪ್ಯಾಚ್ ಮಾಡಿ&lt;/b&gt; ಎಂದು ಬಲವಾಗಿ ಶಿಫಾರಸು ಮಾಡಲಾಗಿದೆ.&lt;p&gt;&lt;br&gt;ನಿರ್ಲಕ್ಷಿಸಿದರೆ, ಈ ಎಚ್ಚರಿಕೆಯನ್ನು ಎರಡು ಬಾರಿ ಮಾತ್ರ ತೋರಿಸಲಾಗುತ್ತದೆ.</string>
<string name="revanced_check_environment_not_same_patching_device">ಬೇರೆ ಸಾಧನದಲ್ಲಿ ಪ್ಯಾಚ್ ಮಾಡಲಾಗಿದೆ</string>
<string name="revanced_check_environment_manager_not_expected_installer">ReVanced Manager ನಿಂದ ಸ್ಥಾಪಿಸಿದ್ದಲ್ಲ</string>
<string name="revanced_check_environment_not_near_patch_time">10 ನಿಮಿಷಗಳಿಗಿಂತ ಮುಂಚೆ ಪ್ಯಾಚ್ ಮಾಡಲಾಗಿದೆ</string>
<string name="revanced_check_environment_not_near_patch_time_days">%s ದಿನಗಳ ಹಿಂದೆ ಪ್ಯಾಚ್ ಮಾಡಲಾಗಿದೆ</string>
<string name="revanced_check_environment_not_near_patch_time_invalid">APK ನಿರ್ಮಾಣ ದಿನಾಂಕವು ಭ್ರಷ್ಟಗೊಂಡಿದೆ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_failed_title">ಪರಿಶೀಲನೆ ವಿಫಲವಾಗಿದೆ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_dialog_open_official_source_button">ಅಧಿಕೃತ ಜಾಲತಾಣ ತೆರೆಯಿರಿ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_dialog_ignore_button">ನಿರ್ಲಕ್ಷಿಸು</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_failed_message">&lt;h5&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ನೀವು ಪ್ಯಾಚ್ ಮಾಡಿದಂತೆ ಕಾಣುತ್ತಿಲ್ಲ.&lt;/h5&gt;&lt;br&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸದೇ ಇರಬಹುದು, &lt;b&gt; ಹಾಗು ಉಪಯೋಗಿಸಲು ಹಾನಿಕಾರಕ ಅಥವಾ ಅಪಾಯಕಾರಿಯಾಗಿರಬಹುದು&lt;/b&gt;.&lt;br&gt;&lt;br&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಮೊದಲೇ ಪ್ಯಾಚ್ ಆಗಿದೆ ಅಥವಾ ಬೇರೆಯವರಿಂದ ಪಡೆದದ್ದು ಎಂದು ಈ ಪರಿಶೀಲನೆಗಳು ಸೂಚಿಸುತ್ತವೆ:&lt;br&gt;&lt;br&gt;&lt;small&gt;%1$s&lt;/small&gt;&lt;br&gt;ನೀವು ದೃಢೀಕೃತ ಮತ್ತು ಸುರಕ್ಷಿತ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಬಳಸುತ್ತಿರುವಿರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಲು &lt;b&gt;ಈ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಿ ಮತ್ತು ನೀವೇ ಪ್ಯಾಚ್ ಮಾಡಿ&lt;/b&gt; ಎಂದು ಬಲವಾಗಿ ಶಿಫಾರಸು ಮಾಡಲಾಗಿದೆ.&lt;p&gt;&lt;br&gt;ನಿರ್ಲಕ್ಷಿಸಿದರೆ, ಈ ಎಚ್ಚರಿಕೆಯನ್ನು ಎರಡು ಬಾರಿ ಮಾತ್ರ ತೋರಿಸಲಾಗುತ್ತದೆ.</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_same_patching_device">ಬೇರೆ ಸಾಧನದಲ್ಲಿ ಪ್ಯಾಚ್ ಮಾಡಲಾಗಿದೆ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_manager_not_expected_installer">ReVanced Manager ನಿಂದ ಸ್ಥಾಪಿಸಿದ್ದಲ್ಲ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time">10 ನಿಮಿಷಗಳಿಗಿಂತ ಮುಂಚೆ ಪ್ಯಾಚ್ ಮಾಡಲಾಗಿದೆ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time_days">%s ದಿನಗಳ ಹಿಂದೆ ಪ್ಯಾಚ್ ಮಾಡಲಾಗಿದೆ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time_invalid">APK ನಿರ್ಮಾಣ ದಿನಾಂಕವು ಭ್ರಷ್ಟಗೊಂಡಿದೆ</string>
</patch>
<patch id="misc.dns.checkWatchHistoryDomainNameResolutionPatch">
</patch>
<patch id="misc.settings.settingsResourcePatch">
<string name="revanced_settings_submenu_title">ಸಂಯೋಜನೆಗಳು</string>
<string name="revanced_settings_confirm_user_dialog_title">ನೀವು ಮುಂದುವರಿಯಲು ಖಚಿತವಾಗಿ ಬಯಸುತ್ತೀರಾ?</string>
<string name="revanced_settings_reset">ಮರುಹೊಂದಿಸಿ</string>
<string name="revanced_settings_reset_color">ಬಣ್ಣ ಮರುಹೊಂದಿಸಿ</string>
<string name="revanced_settings_color_invalid">ಅಮಾನ್ಯ ಬಣ್ಣ</string>
<string name="revanced_settings_restart_title">ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_submenu_title">ಸಂಯೋಜನೆಗಳು</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_confirm_user_dialog_title">ನೀವು ಮುಂದುವರಿಯಲು ಖಚಿತವಾಗಿ ಬಯಸುತ್ತೀರಾ?</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_reset">ಮರುಹೊಂದಿಸಿ</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_reset_color">ಬಣ್ಣ ಮರುಹೊಂದಿಸಿ</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_color_invalid">ಅಮಾನ್ಯ ಬಣ್ಣ</string>
<string name="shared.misc.settings.settingsResourcePatch.revanced_settings_restart_title">ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ</string>
<!-- Settings about dialog. -->
<!-- NOTE: the about strings above are duplicated in the TikTok about screen code,
and changes made here must also be made there. -->
@ -244,6 +244,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -224,12 +224,14 @@ Second \"item\" text"</string>
<!-- Translations of this should not be longer than the original English text, otherwise the text can be clipped and not entirely shown. -->
</patch>
<patch id="misc.announcements.announcementsPatch">
<string name="revanced_announcements_dialog_dismiss">Melepaskan</string>
<string name="youtube.misc.announcements.announcementsPatch.revanced_announcements_dialog_dismiss">Melepaskan</string>
</patch>
<patch id="misc.loopvideo.loopVideoPatch">
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -26,8 +26,8 @@ Second \"item\" text"</string>
<!-- Translations of this should be identical to revanced_custom_branding_name_entry_5 -->
</patch>
<patch id="misc.checks.checkEnvironmentPatch">
<string name="revanced_check_environment_manager_not_expected_installer">ReVanced Manager द्वारा स्थापित छैन</string>
<string name="revanced_check_environment_not_near_patch_time_invalid">APK निर्माण मिति खराब भएको छ</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_manager_not_expected_installer">ReVanced Manager द्वारा स्थापित छैन</string>
<string name="shared.misc.checks.checkEnvironmentPatch.revanced_check_environment_not_near_patch_time_invalid">APK निर्माण मिति खराब भएको छ</string>
</patch>
<patch id="misc.dns.checkWatchHistoryDomainNameResolutionPatch">
</patch>
@ -231,6 +231,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--
All strings must have a unique path, even if the same string is declared in two different apps.
@ -229,6 +229,8 @@ Second \"item\" text"</string>
</patch>
<patch id="misc.loopvideo.button.loopVideoButtonPatch">
</patch>
<patch id="misc.audiofocus.pauseOnAudioInterruptPatch">
</patch>
<patch id="misc.dimensions.spoof.spoofDeviceDimensionsPatch">
</patch>
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">

Some files were not shown because too many files have changed in this diff Show more