diff --git a/src/main/kotlin/app/revanced/extensions/Extensions.kt b/src/main/kotlin/app/revanced/extensions/Extensions.kt index cf3745251..6a950ca42 100644 --- a/src/main/kotlin/app/revanced/extensions/Extensions.kt +++ b/src/main/kotlin/app/revanced/extensions/Extensions.kt @@ -1,6 +1,6 @@ package app.revanced.extensions -import app.revanced.patcher.data.impl.BytecodeData +import app.revanced.patcher.data.impl.ResourceData import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.patch.PatchResultError import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod @@ -14,6 +14,8 @@ import org.jf.dexlib2.builder.instruction.BuilderInstruction21t import org.jf.dexlib2.builder.instruction.BuilderInstruction35c import org.jf.dexlib2.immutable.reference.ImmutableMethodReference import org.w3c.dom.Node +import java.io.OutputStream +import java.nio.file.Files internal fun MutableMethodImplementation.injectHideCall( index: Int, @@ -100,37 +102,65 @@ internal fun MutableMethod.injectConsumableEventHook(hookRef: ImmutableMethodRef } /** - * Insert instructions into a named method + * inject resources into the patched app * - * @param targetClass the name of the class of which the method is a member - * @param targetMethod the name of the method to insert into - * @param index index to insert the instructions at. If the index is negative, it is used as an offset to the last method (so -1 inserts at the end of the method) - * @param instructions the smali instructions to insert (they'll be compiled by MutableMethod.addInstructions) + * @param classLoader classloader to use for loading the resources + * @param patchDirectoryPath path to the files. this will be the directory you created under the 'resources' source folder + * @param resourceType the resource type, for example 'drawable'. this has to match both the source and the target + * @param resourceFileNames names of all resources of this type to inject */ -internal fun BytecodeData.injectIntoNamedMethod( - targetClass: String, - targetMethod: String, - index: Int, - instructions: String +fun ResourceData.injectResources( + classLoader: ClassLoader, + patchDirectoryPath: String, + resourceType: String, + resourceFileNames: List ) { - var injections = 0 - this.classes.filter { it.type.endsWith("$targetClass;") }.forEach { classDef -> - this.proxy(classDef).resolve().methods.filter { it.name == targetMethod }.forEach { methodDef -> - // if index is negative, interpret as an offset from the back - var insertIndex = index - if (insertIndex < 0) { - insertIndex += methodDef.implementation!!.instructions.size - } + resourceFileNames.forEach { name -> + val relativePath = "$resourceType/$name" + val sourceRes = classLoader.getResourceAsStream("$patchDirectoryPath/$relativePath") + ?: throw PatchResultError("could not open resource '$patchDirectoryPath/$relativePath'") - // insert instructions - methodDef.addInstructions(insertIndex, instructions) - injections++ - } + Files.copy( + sourceRes, + this["res"].resolve(relativePath).toPath() + ) } +} - // fail if nothing was injected - if (injections <= 0) { - throw PatchResultError("failed to inject into $targetClass.$targetMethod: no targets were found") +/** + * inject strings into the patched app + * + * @param classLoader classloader to use for loading the resources + * @param patchDirectoryPath path to the files. this will be the directory you created under the 'resources' source folder + * @param languageIdentifier ISO 639-2 two- letter language code identifier (aka the one android uses for values directory) + */ +fun ResourceData.injectStrings( + classLoader: ClassLoader, + patchDirectoryPath: String, + languageIdentifier: String? = null, +) { + val relativePath = + if (languageIdentifier.isNullOrBlank()) "values/strings.xml" else "values/strings-$languageIdentifier.xml" + + // open source strings.xml + val sourceInputStream = classLoader.getResourceAsStream("$patchDirectoryPath/$relativePath") + ?: throw PatchResultError("failed to open '$patchDirectoryPath/$relativePath'") + xmlEditor[sourceInputStream, OutputStream.nullOutputStream()].use { sourceStringsXml -> + val strings = sourceStringsXml.file.getElementsByTagName("resources").item(0).childNodes + + // open target strings.xml + xmlEditor["res/$relativePath"].use { targetStringsXml -> + val targetFile = targetStringsXml.file + val targetRootNode = targetFile.getElementsByTagName("resources").item(0) + + // process all children strings in the source + for (i in 0 until strings.length) { + // clone the node from source to target + val node = strings.item(i).cloneNode(true) + targetFile.adoptNode(node) + targetRootNode.appendChild(node) + } + } } } diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/patch/FensterPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/patch/FensterPatch.kt deleted file mode 100644 index 43ce96c0e..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/patch/FensterPatch.kt +++ /dev/null @@ -1,121 +0,0 @@ -package app.revanced.patches.youtube.interaction.fenster.patch - -import app.revanced.extensions.injectConsumableEventHook -import app.revanced.extensions.injectIntoNamedMethod -import app.revanced.patcher.annotation.Description -import app.revanced.patcher.annotation.Name -import app.revanced.patcher.annotation.Version -import app.revanced.patcher.data.impl.BytecodeData -import app.revanced.patcher.extensions.addInstruction -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultError -import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.patch.annotations.Dependencies -import app.revanced.patcher.patch.annotations.Patch -import app.revanced.patcher.patch.impl.BytecodePatch -import app.revanced.patches.youtube.interaction.fenster.annotation.FensterCompatibility -import app.revanced.patches.youtube.interaction.fenster.fingerprints.UpdatePlayerTypeFingerprint -import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch -import org.jf.dexlib2.immutable.reference.ImmutableMethodReference - -@Patch -@Name("fenster-swipe-controls") -@Description("Adds volume and brightness swipe controls.") -@FensterCompatibility -@Version("0.0.1") -@Dependencies(dependencies = [IntegrationsPatch::class]) -class FensterPatch : BytecodePatch( - listOf( - UpdatePlayerTypeFingerprint - ) -) { - override fun execute(data: BytecodeData): PatchResult { - // hook WatchWhileActivity.onStart (main activity lifecycle hook) - data.injectIntoNamedMethod( - "com/google/android/apps/youtube/app/watchwhile/WatchWhileActivity", - "onStart", - 0, - "invoke-static { p0 }, Lapp/revanced/integrations/patches/FensterSwipePatch;->WatchWhileActivity_onStartHookEX(Ljava/lang/Object;)V" - ) - - // hook YoutubePlayerOverlaysLayout.onFinishInflate (player overlays init hook) - data.injectIntoNamedMethod( - "com/google/android/apps/youtube/app/common/player/overlay/YouTubePlayerOverlaysLayout", - "onFinishInflate", - -2, - "invoke-static { p0 }, Lapp/revanced/integrations/patches/FensterSwipePatch;->YouTubePlayerOverlaysLayout_onFinishInflateHookEX(Ljava/lang/Object;)V" - ) - - // hook YoutubePlayerOverlaysLayout.UpdatePlayerType - injectUpdatePlayerTypeHook( - UpdatePlayerTypeFingerprint.result!!, - "com/google/android/apps/youtube/app/common/player/overlay/YouTubePlayerOverlaysLayout" - ) - - // hook NextGenWatchLayout.onTouchEvent and NextGenWatchLayout.onInterceptTouchEvent (player touch event hook) - injectWatchLayoutTouchHooks( - data, - "com/google/android/apps/youtube/app/watch/nextgenwatch/ui/NextGenWatchLayout" - ) - - return PatchResultSuccess() - } - - @Suppress("SameParameterValue") - private fun injectUpdatePlayerTypeHook(fingerPrintResult: MethodFingerprintResult, targetClass: String) { - // validate fingerprint found the right class - if (!fingerPrintResult.classDef.type.endsWith("$targetClass;")) { - throw PatchResultError("$targetClass.UpdatePlayerType fingerprint could not be validated") - } - - // insert the hook - fingerPrintResult.mutableMethod.addInstruction( - 0, - "invoke-static { p1 }, Lapp/revanced/integrations/patches/FensterSwipePatch;->YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V" - ) - } - - /** - * Inject onTouch event hooks into the watch layout class - * - * @param data bytecode data - * @param targetClass watch layout class name - */ - @Suppress("SameParameterValue") - private fun injectWatchLayoutTouchHooks(data: BytecodeData, targetClass: String) { - var touchHooksCount = 0 - data.classes.filter { it.type.endsWith("$targetClass;") }.forEach { classDef -> - // hook onTouchEvent - data.proxy(classDef).resolve().methods.filter { it.name == "onTouchEvent" }.forEach { methodDef -> - touchHooksCount++ - methodDef.injectConsumableEventHook( - ImmutableMethodReference( - "Lapp/revanced/integrations/patches/FensterSwipePatch;", - "NextGenWatchLayout_onTouchEventHookEX", - listOf("Ljava/lang/Object;", "Ljava/lang/Object;"), - "Z" - ) - ) - } - - // hook onInterceptTouchEvent - data.proxy(classDef).resolve().methods.filter { it.name == "onInterceptTouchEvent" }.forEach { methodDef -> - touchHooksCount++ - methodDef.injectConsumableEventHook( - ImmutableMethodReference( - "Lapp/revanced/integrations/patches/FensterSwipePatch;", - "NextGenWatchLayout_onInterceptTouchEventHookEX", - listOf("Ljava/lang/Object;", "Ljava/lang/Object;"), - "Z" - ) - ) - } - } - - // fail if no touch hooks were inserted - if (touchHooksCount <= 0) { - throw PatchResultError("failed to inject onTouchEvent hook into NextGenWatchLayout: none found") - } - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/annotation/SwipeControlsCompatibility.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/annotation/SwipeControlsCompatibility.kt new file mode 100644 index 000000000..1d7073ece --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/annotation/SwipeControlsCompatibility.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.youtube.interaction.swipecontrols.annotation + +import app.revanced.patcher.annotation.Compatibility +import app.revanced.patcher.annotation.Package + +@Compatibility( + [Package( + "com.google.android.youtube", arrayOf("17.24.34", "17.25.34") + )] +) +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +internal annotation class SwipeControlsCompatibility diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/fingerprints/WatchWhileOnStartFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/fingerprints/WatchWhileOnStartFingerprint.kt new file mode 100644 index 000000000..6a72387ca --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/fingerprints/WatchWhileOnStartFingerprint.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.youtube.interaction.swipecontrols.fingerprints + +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod +import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import app.revanced.patches.youtube.interaction.swipecontrols.annotation.SwipeControlsCompatibility + +@Name("watch-while-onStart-fingerprint") +@MatchingMethod( + "LWatchWhileActivity;", "onCreate" +) +@DirectPatternScanMethod +@SwipeControlsCompatibility +@Version("0.0.1") +object WatchWhileOnStartFingerprint : MethodFingerprint( + null, null, null, null, null, { methodDef -> + methodDef.definingClass.endsWith("WatchWhileActivity;") && methodDef.name == "onStart" + } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/SwipeControlsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/SwipeControlsPatch.kt new file mode 100644 index 000000000..31117035e --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/SwipeControlsPatch.kt @@ -0,0 +1,42 @@ +package app.revanced.patches.youtube.interaction.swipecontrols.patch + +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.data.impl.BytecodeData +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.patch.annotations.Dependencies +import app.revanced.patcher.patch.annotations.Patch +import app.revanced.patcher.patch.impl.BytecodePatch +import app.revanced.patches.youtube.interaction.swipecontrols.annotation.SwipeControlsCompatibility +import app.revanced.patches.youtube.interaction.swipecontrols.fingerprints.WatchWhileOnStartFingerprint +import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch +import app.revanced.patches.youtube.misc.playertype.patch.PlayerTypeHookPatch + +@Patch +@Name("swipe-controls") +@Description("Adds volume and brightness swipe controls.") +@SwipeControlsCompatibility +@Version("0.0.2") +@Dependencies( + dependencies = [ + IntegrationsPatch::class, + PlayerTypeHookPatch::class, + SwipeControlsResourcesPatch::class + ] +) +class SwipeControlsPatch : BytecodePatch( + listOf( + WatchWhileOnStartFingerprint + ) +) { + override fun execute(data: BytecodeData): PatchResult { + WatchWhileOnStartFingerprint.result!!.mutableMethod.addInstruction( + 0, + "invoke-static { p0 }, Lapp/revanced/integrations/patches/SwipeControlsPatch;->WatchWhileActivity_onStartHookEX(Ljava/lang/Object;)V" + ) + return PatchResultSuccess() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/SwipeControlsResourcesPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/SwipeControlsResourcesPatch.kt new file mode 100644 index 000000000..318640f20 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/SwipeControlsResourcesPatch.kt @@ -0,0 +1,32 @@ +package app.revanced.patches.youtube.interaction.swipecontrols.patch + +import app.revanced.extensions.injectResources +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.data.impl.ResourceData +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.patch.impl.ResourcePatch +import app.revanced.patches.youtube.interaction.swipecontrols.annotation.SwipeControlsCompatibility + +@Name("swipe-controls-resource-patch") +@SwipeControlsCompatibility +@Version("0.0.1") +class SwipeControlsResourcesPatch : ResourcePatch() { + override fun execute(data: ResourceData): PatchResult { + val resourcesDir = "swipecontrols" + + data.injectResources( + this.javaClass.classLoader, + resourcesDir, + "drawable", + listOf( + "ic_sc_brightness_auto", + "ic_sc_brightness_manual", + "ic_sc_volume_mute", + "ic_sc_volume_normal" + ).map { "$it.xml" } + ) + return PatchResultSuccess() + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/annotation/FensterCompatibility.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/annotation/PlayerTypeHookCompatibility.kt similarity index 71% rename from src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/annotation/FensterCompatibility.kt rename to src/main/kotlin/app/revanced/patches/youtube/misc/playertype/annotation/PlayerTypeHookCompatibility.kt index f6d055b91..d57a31c92 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/annotation/FensterCompatibility.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/annotation/PlayerTypeHookCompatibility.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.interaction.fenster.annotation +package app.revanced.patches.youtube.misc.playertype.annotation import app.revanced.patcher.annotation.Compatibility import app.revanced.patcher.annotation.Package @@ -10,4 +10,4 @@ import app.revanced.patcher.annotation.Package ) @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -internal annotation class FensterCompatibility +internal annotation class PlayerTypeHookCompatibility diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/fingerprints/UpdatePlayerTypeFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/fingerprint/UpdatePlayerTypeFingerprint.kt similarity index 82% rename from src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/fingerprints/UpdatePlayerTypeFingerprint.kt rename to src/main/kotlin/app/revanced/patches/youtube/misc/playertype/fingerprint/UpdatePlayerTypeFingerprint.kt index eb6a5045f..7bdac3ab8 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/interaction/fenster/fingerprints/UpdatePlayerTypeFingerprint.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/fingerprint/UpdatePlayerTypeFingerprint.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.interaction.fenster.fingerprints +package app.revanced.patches.youtube.misc.playertype.fingerprint import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Version @@ -6,17 +6,18 @@ import app.revanced.patcher.extensions.or import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import app.revanced.patches.youtube.interaction.fenster.annotation.FensterCompatibility +import app.revanced.patches.youtube.interaction.swipecontrols.annotation.SwipeControlsCompatibility import org.jf.dexlib2.AccessFlags import org.jf.dexlib2.Opcode +//TODO constrain to only match in YoutubePlayerOverlaysLayout? @Name("update-player-type-fingerprint") @MatchingMethod( "LYoutubePlayerOverlaysLayout;", "nM" ) @FuzzyPatternScanMethod(2) -@FensterCompatibility +@SwipeControlsCompatibility @Version("0.0.1") object UpdatePlayerTypeFingerprint : MethodFingerprint( "V", diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/patch/PlayerTypeHookPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/patch/PlayerTypeHookPatch.kt new file mode 100644 index 000000000..bd122fe99 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/patch/PlayerTypeHookPatch.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.youtube.misc.playertype.patch + +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.data.impl.BytecodeData +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.patch.annotations.Dependencies +import app.revanced.patcher.patch.impl.BytecodePatch +import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch +import app.revanced.patches.youtube.misc.playertype.annotation.PlayerTypeHookCompatibility +import app.revanced.patches.youtube.misc.playertype.fingerprint.UpdatePlayerTypeFingerprint + +@Name("player-type-hook") +@Description("hook to get the current player type of WatchWhileActivity") +@PlayerTypeHookCompatibility +@Version("0.0.1") +@Dependencies(dependencies = [IntegrationsPatch::class]) +class PlayerTypeHookPatch : BytecodePatch( + listOf( + UpdatePlayerTypeFingerprint + ) +) { + override fun execute(data: BytecodeData): PatchResult { + // hook YouTubePlayerOverlaysLayout.updatePlayerLayout() + UpdatePlayerTypeFingerprint.result!!.mutableMethod.addInstruction( + 0, + "invoke-static { p1 }, Lapp/revanced/integrations/patches/PlayerTypeHookPatch;->YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V" + ) + return PatchResultSuccess() + } +} diff --git a/src/main/resources/swipecontrols/drawable/ic_sc_brightness_auto.xml b/src/main/resources/swipecontrols/drawable/ic_sc_brightness_auto.xml new file mode 100644 index 000000000..469b33596 --- /dev/null +++ b/src/main/resources/swipecontrols/drawable/ic_sc_brightness_auto.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/resources/swipecontrols/drawable/ic_sc_brightness_manual.xml b/src/main/resources/swipecontrols/drawable/ic_sc_brightness_manual.xml new file mode 100644 index 000000000..2f6c7072d --- /dev/null +++ b/src/main/resources/swipecontrols/drawable/ic_sc_brightness_manual.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/resources/swipecontrols/drawable/ic_sc_volume_mute.xml b/src/main/resources/swipecontrols/drawable/ic_sc_volume_mute.xml new file mode 100644 index 000000000..73dc595f4 --- /dev/null +++ b/src/main/resources/swipecontrols/drawable/ic_sc_volume_mute.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/resources/swipecontrols/drawable/ic_sc_volume_normal.xml b/src/main/resources/swipecontrols/drawable/ic_sc_volume_normal.xml new file mode 100644 index 000000000..30dff4be1 --- /dev/null +++ b/src/main/resources/swipecontrols/drawable/ic_sc_volume_normal.xml @@ -0,0 +1,5 @@ + + +