mirror of
https://github.com/revanced/revanced-patches
synced 2024-11-10 11:29:23 +01:00
feat(twitch): settings
patch (#1075)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
65fddd52e2
commit
6e95b86e50
@ -19,8 +19,10 @@ internal data class ArrayResource(
|
|||||||
|
|
||||||
override fun serialize(ownerDocument: Document, resourceCallback: ((IResource) -> Unit)?): Element {
|
override fun serialize(ownerDocument: Document, resourceCallback: ((IResource) -> Unit)?): Element {
|
||||||
return super.serialize(ownerDocument, resourceCallback).apply {
|
return super.serialize(ownerDocument, resourceCallback).apply {
|
||||||
|
setAttribute("name", name)
|
||||||
|
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
setAttribute("name", item.also { resourceCallback?.invoke(it) }.name)
|
resourceCallback?.invoke(item)
|
||||||
|
|
||||||
this.appendChild(ownerDocument.createElement("item").also { itemNode ->
|
this.appendChild(ownerDocument.createElement("item").also { itemNode ->
|
||||||
itemNode.textContent = item.value
|
itemNode.textContent = item.value
|
||||||
|
@ -15,7 +15,7 @@ import org.w3c.dom.Element
|
|||||||
internal open class PreferenceCategory(
|
internal open class PreferenceCategory(
|
||||||
key: String,
|
key: String,
|
||||||
title: StringResource,
|
title: StringResource,
|
||||||
val preferences: List<BasePreference>
|
var preferences: List<BasePreference>
|
||||||
) : BasePreference(key, title) {
|
) : BasePreference(key, title) {
|
||||||
override val tag: String = "PreferenceCategory"
|
override val tag: String = "PreferenceCategory"
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import org.w3c.dom.Element
|
|||||||
internal open class PreferenceScreen(
|
internal open class PreferenceScreen(
|
||||||
key: String,
|
key: String,
|
||||||
title: StringResource,
|
title: StringResource,
|
||||||
val preferences: List<BasePreference>,
|
var preferences: List<BasePreference>,
|
||||||
val summary: StringResource? = null
|
val summary: StringResource? = null
|
||||||
) : BasePreference(key, title) {
|
) : BasePreference(key, title) {
|
||||||
override val tag: String = "PreferenceScreen"
|
override val tag: String = "PreferenceScreen"
|
||||||
|
@ -26,6 +26,17 @@ abstract class AbstractSettingsResourcePatch(
|
|||||||
private val sourceDirectory: String,
|
private val sourceDirectory: String,
|
||||||
) : ResourcePatch {
|
) : ResourcePatch {
|
||||||
override fun execute(context: ResourceContext): PatchResult {
|
override fun execute(context: ResourceContext): PatchResult {
|
||||||
|
/*
|
||||||
|
* used for self-restart
|
||||||
|
*/
|
||||||
|
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||||
|
editor.file.getElementsByTagName("manifest").item(0).also {
|
||||||
|
it.appendChild(it.ownerDocument.createElement("uses-permission").also { element ->
|
||||||
|
element.setAttribute("android:name", "android.permission.SCHEDULE_EXACT_ALARM")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* copy preference template from source dir */
|
/* copy preference template from source dir */
|
||||||
context.copyResources(
|
context.copyResources(
|
||||||
sourceDirectory,
|
sourceDirectory,
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.annotations
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Compatibility
|
||||||
|
import app.revanced.patcher.annotation.Package
|
||||||
|
|
||||||
|
@Compatibility([Package("tv.twitch.android.app")])
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
internal annotation class SettingsCompatibility
|
@ -0,0 +1,195 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.bytecode.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Description
|
||||||
|
import app.revanced.patcher.annotation.Name
|
||||||
|
import app.revanced.patcher.annotation.Version
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.extensions.*
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.PatchResult
|
||||||
|
import app.revanced.patcher.patch.PatchResultSuccess
|
||||||
|
import app.revanced.patcher.patch.annotations.DependsOn
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
|
import app.revanced.patches.shared.settings.preference.impl.PreferenceCategory
|
||||||
|
import app.revanced.patches.shared.settings.preference.impl.StringResource
|
||||||
|
import app.revanced.patches.shared.settings.util.AbstractPreferenceScreen
|
||||||
|
import app.revanced.patches.twitch.misc.integrations.patch.IntegrationsPatch
|
||||||
|
import app.revanced.patches.twitch.misc.settings.annotations.SettingsCompatibility
|
||||||
|
import app.revanced.patches.twitch.misc.settings.components.CustomPreferenceCategory
|
||||||
|
import app.revanced.patches.twitch.misc.settings.fingerprints.*
|
||||||
|
import app.revanced.patches.twitch.misc.settings.resource.patch.SettingsResourcePatch
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableField
|
||||||
|
|
||||||
|
@Patch
|
||||||
|
@DependsOn([IntegrationsPatch::class, SettingsResourcePatch::class])
|
||||||
|
@Name("settings")
|
||||||
|
@Description("Adds settings menu to Twitch.")
|
||||||
|
@SettingsCompatibility
|
||||||
|
@Version("0.0.1")
|
||||||
|
class SettingsPatch : BytecodePatch(
|
||||||
|
listOf(
|
||||||
|
SettingsActivityOnCreateFingerprint,
|
||||||
|
SettingsMenuItemEnumFingerprint,
|
||||||
|
MenuGroupsUpdatedFingerprint,
|
||||||
|
MenuGroupsOnClickFingerprint
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
override fun execute(context: BytecodeContext): PatchResult {
|
||||||
|
// Hook onCreate to handle fragment creation
|
||||||
|
with(SettingsActivityOnCreateFingerprint.result!!) {
|
||||||
|
val insertIndex = mutableMethod.implementation!!.instructions.size - 2
|
||||||
|
mutableMethod.addInstructions(
|
||||||
|
insertIndex,
|
||||||
|
"""
|
||||||
|
invoke-static {p0}, $SETTINGS_HOOKS_CLASS->handleSettingsCreation(Landroidx/appcompat/app/AppCompatActivity;)Z
|
||||||
|
move-result v0
|
||||||
|
if-eqz v0, :no_rv_settings_init
|
||||||
|
return-void
|
||||||
|
""",
|
||||||
|
listOf(ExternalLabel("no_rv_settings_init", mutableMethod.instruction(insertIndex)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new menu item for settings menu
|
||||||
|
with(SettingsMenuItemEnumFingerprint.result!!) {
|
||||||
|
injectMenuItem(
|
||||||
|
REVANCED_SETTINGS_MENU_ITEM_NAME,
|
||||||
|
REVANCED_SETTINGS_MENU_ITEM_ID,
|
||||||
|
REVANCED_SETTINGS_MENU_ITEM_TITLE_RES,
|
||||||
|
REVANCED_SETTINGS_MENU_ITEM_ICON_RES
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept settings menu creation and add new menu item
|
||||||
|
with(MenuGroupsUpdatedFingerprint.result!!) {
|
||||||
|
mutableMethod.addInstructions(
|
||||||
|
0,
|
||||||
|
"""
|
||||||
|
sget-object v0, $MENU_ITEM_ENUM_CLASS->$REVANCED_SETTINGS_MENU_ITEM_NAME:$MENU_ITEM_ENUM_CLASS
|
||||||
|
invoke-static {p1, v0}, $SETTINGS_HOOKS_CLASS->handleSettingMenuCreation(Ljava/util/List;Ljava/lang/Object;)Ljava/util/List;
|
||||||
|
move-result-object p1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept onclick events for the settings menu
|
||||||
|
with(MenuGroupsOnClickFingerprint.result!!) {
|
||||||
|
val insertIndex = 0
|
||||||
|
mutableMethod.addInstructions(
|
||||||
|
insertIndex,
|
||||||
|
"""
|
||||||
|
invoke-static {p1}, $SETTINGS_HOOKS_CLASS->handleSettingMenuOnClick(Ljava/lang/Enum;)Z
|
||||||
|
move-result p2
|
||||||
|
if-eqz p2, :no_rv_settings_onclick
|
||||||
|
sget-object p1, $MENU_DISMISS_EVENT_CLASS->INSTANCE:$MENU_DISMISS_EVENT_CLASS
|
||||||
|
invoke-virtual {p0, p1}, Ltv/twitch/android/core/mvp/viewdelegate/RxViewDelegate;->pushEvent(Ltv/twitch/android/core/mvp/viewdelegate/ViewDelegateEvent;)V
|
||||||
|
return-void
|
||||||
|
""",
|
||||||
|
listOf(ExternalLabel("no_rv_settings_onclick", mutableMethod.instruction(insertIndex)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addString("revanced_settings", "ReVanced Settings", false)
|
||||||
|
addString("revanced_reboot_message", "Twitch needs to restart to apply your changes. Restart now?", false)
|
||||||
|
addString("revanced_reboot", "Restart", false)
|
||||||
|
addString("revanced_cancel", "Cancel", false)
|
||||||
|
|
||||||
|
return PatchResultSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
fun addString(identifier: String, value: String, formatted: Boolean = true) =
|
||||||
|
SettingsResourcePatch.addString(identifier, value, formatted)
|
||||||
|
|
||||||
|
fun addPreferenceScreen(preferenceScreen: app.revanced.patches.shared.settings.preference.impl.PreferenceScreen) =
|
||||||
|
SettingsResourcePatch.addPreferenceScreen(preferenceScreen)
|
||||||
|
|
||||||
|
/* Private members */
|
||||||
|
private const val REVANCED_SETTINGS_MENU_ITEM_NAME = "RevancedSettings"
|
||||||
|
private const val REVANCED_SETTINGS_MENU_ITEM_ID = 0x7
|
||||||
|
private const val REVANCED_SETTINGS_MENU_ITEM_TITLE_RES = "revanced_settings"
|
||||||
|
private const val REVANCED_SETTINGS_MENU_ITEM_ICON_RES = "ic_settings"
|
||||||
|
|
||||||
|
private const val MENU_ITEM_ENUM_CLASS = "Ltv/twitch/android/feature/settings/menu/SettingsMenuItem;"
|
||||||
|
private const val MENU_DISMISS_EVENT_CLASS = "Ltv/twitch/android/feature/settings/menu/SettingsMenuViewDelegate\$Event\$OnDismissClicked;"
|
||||||
|
|
||||||
|
private const val INTEGRATIONS_PACKAGE = "app/revanced/twitch"
|
||||||
|
private const val SETTINGS_HOOKS_CLASS = "L$INTEGRATIONS_PACKAGE/settingsmenu/SettingsHooks;"
|
||||||
|
private const val REVANCED_UTILS_CLASS = "L$INTEGRATIONS_PACKAGE/utils/ReVancedUtils;"
|
||||||
|
|
||||||
|
private fun MethodFingerprintResult.injectMenuItem(
|
||||||
|
name: String,
|
||||||
|
value: Int,
|
||||||
|
titleResourceName: String,
|
||||||
|
iconResourceName: String
|
||||||
|
) {
|
||||||
|
// Add new static enum member field
|
||||||
|
mutableClass.staticFields.add(
|
||||||
|
ImmutableField(
|
||||||
|
mutableMethod.definingClass,
|
||||||
|
name,
|
||||||
|
MENU_ITEM_ENUM_CLASS,
|
||||||
|
AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.ENUM or AccessFlags.STATIC,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).toMutable()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add initializer for the new enum member
|
||||||
|
mutableMethod.addInstructions(
|
||||||
|
mutableMethod.implementation!!.instructions.size - 4,
|
||||||
|
"""
|
||||||
|
new-instance v0, $MENU_ITEM_ENUM_CLASS
|
||||||
|
const-string v1, "$titleResourceName"
|
||||||
|
invoke-static {v1}, $REVANCED_UTILS_CLASS->getStringId(Ljava/lang/String;)I
|
||||||
|
move-result v1
|
||||||
|
const-string v3, "$iconResourceName"
|
||||||
|
invoke-static {v3}, $REVANCED_UTILS_CLASS->getDrawableId(Ljava/lang/String;)I
|
||||||
|
move-result v3
|
||||||
|
const-string v4, "$name"
|
||||||
|
const/4 v5, $value
|
||||||
|
invoke-direct {v0, v4, v5, v1, v3}, $MENU_ITEM_ENUM_CLASS-><init>(Ljava/lang/String;III)V
|
||||||
|
sput-object v0, $MENU_ITEM_ENUM_CLASS->$name:$MENU_ITEM_ENUM_CLASS
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference screens patches should add their settings to.
|
||||||
|
*/
|
||||||
|
internal object PreferenceScreen : AbstractPreferenceScreen() {
|
||||||
|
val ADS = CustomScreen("ads", "Ads", "Ad blocking settings")
|
||||||
|
val CHAT = CustomScreen("chat", "Chat", "Chat settings")
|
||||||
|
val MISC = CustomScreen("misc", "Misc", "Miscellaneous patches")
|
||||||
|
|
||||||
|
internal class CustomScreen(key: String, title: String, summary: String) : Screen(key, title, summary) {
|
||||||
|
/* Categories */
|
||||||
|
val GENERAL = CustomCategory("general", "General settings")
|
||||||
|
val OTHER = CustomCategory("other", "Other settings")
|
||||||
|
val CLIENT_SIDE = CustomCategory("client_ads", "Client-side ads")
|
||||||
|
|
||||||
|
internal inner class CustomCategory(key: String, title: String) : Screen.Category(key, title) {
|
||||||
|
/* For Twitch, we need to load our CustomPreferenceCategory class instead of the default one. */
|
||||||
|
override fun transform(): PreferenceCategory {
|
||||||
|
return CustomPreferenceCategory(
|
||||||
|
key,
|
||||||
|
StringResource("${key}_title", title),
|
||||||
|
preferences.sortedBy { it.title.value }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun commit(screen: app.revanced.patches.shared.settings.preference.impl.PreferenceScreen) {
|
||||||
|
addPreferenceScreen(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = PreferenceScreen.close()
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.components
|
||||||
|
|
||||||
|
import app.revanced.patches.shared.settings.preference.BasePreference
|
||||||
|
import app.revanced.patches.shared.settings.preference.impl.PreferenceCategory
|
||||||
|
import app.revanced.patches.shared.settings.preference.impl.StringResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customized preference category for Twitch.
|
||||||
|
*
|
||||||
|
* @param key The key of the preference.
|
||||||
|
* @param title The title of the preference.
|
||||||
|
* @param preferences Child preferences of this category.
|
||||||
|
*/
|
||||||
|
internal open class CustomPreferenceCategory(
|
||||||
|
key: String,
|
||||||
|
title: StringResource,
|
||||||
|
preferences: List<BasePreference>
|
||||||
|
) : PreferenceCategory(key, title, preferences) {
|
||||||
|
override val tag: String = "app.revanced.twitch.settingsmenu.preference.CustomPreferenceCategory"
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
|
||||||
|
object MenuGroupsOnClickFingerprint : MethodFingerprint(
|
||||||
|
"V",
|
||||||
|
AccessFlags.PRIVATE or AccessFlags.STATIC or AccessFlags.FINAL,
|
||||||
|
listOf("L", "L", "L"),
|
||||||
|
customFingerprint = { methodDef ->
|
||||||
|
methodDef.definingClass.endsWith("/SettingsMenuViewDelegate;")
|
||||||
|
&& methodDef.name.contains("render")
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
|
object MenuGroupsUpdatedFingerprint : MethodFingerprint(
|
||||||
|
customFingerprint = { methodDef ->
|
||||||
|
methodDef.definingClass.endsWith("/SettingsMenuPresenter\$Event\$MenuGroupsUpdated;")
|
||||||
|
&& methodDef.name == "<init>"
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
|
object SettingsActivityOnCreateFingerprint : MethodFingerprint(
|
||||||
|
customFingerprint = { methodDef ->
|
||||||
|
methodDef.definingClass.endsWith("/SettingsActivity;") &&
|
||||||
|
methodDef.name == "onCreate"
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.fingerprints
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
|
object SettingsMenuItemEnumFingerprint : MethodFingerprint(
|
||||||
|
customFingerprint = { methodDef ->
|
||||||
|
methodDef.definingClass.endsWith("/SettingsMenuItem;") && methodDef.name == "<clinit>"
|
||||||
|
}
|
||||||
|
)
|
@ -0,0 +1,44 @@
|
|||||||
|
package app.revanced.patches.twitch.misc.settings.resource.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.annotation.Name
|
||||||
|
import app.revanced.patcher.annotation.Version
|
||||||
|
import app.revanced.patches.shared.settings.preference.impl.ArrayResource
|
||||||
|
import app.revanced.patches.shared.settings.preference.impl.PreferenceScreen
|
||||||
|
import app.revanced.patches.shared.settings.resource.patch.AbstractSettingsResourcePatch
|
||||||
|
import app.revanced.patches.twitch.misc.settings.annotations.SettingsCompatibility
|
||||||
|
|
||||||
|
@Name("settings-resource-patch")
|
||||||
|
@SettingsCompatibility
|
||||||
|
@Version("0.0.1")
|
||||||
|
class SettingsResourcePatch : AbstractSettingsResourcePatch(
|
||||||
|
"revanced_prefs",
|
||||||
|
"twitch/settings"
|
||||||
|
) {
|
||||||
|
internal companion object {
|
||||||
|
/* Companion delegates */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new string to the resources.
|
||||||
|
*
|
||||||
|
* @param identifier The key of the string.
|
||||||
|
* @param value The value of the string.
|
||||||
|
* @throws IllegalArgumentException if the string already exists.
|
||||||
|
*/
|
||||||
|
fun addString(identifier: String, value: String, formatted: Boolean) =
|
||||||
|
AbstractSettingsResourcePatch.addString(identifier, value, formatted)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an array to the resources.
|
||||||
|
*
|
||||||
|
* @param arrayResource The array resource to add.
|
||||||
|
*/
|
||||||
|
fun addArray(arrayResource: ArrayResource) = AbstractSettingsResourcePatch.addArray(arrayResource)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a preference to the settings.
|
||||||
|
*
|
||||||
|
* @param preferenceScreen The name of the preference screen.
|
||||||
|
*/
|
||||||
|
fun addPreferenceScreen(preferenceScreen: PreferenceScreen) = AbstractSettingsResourcePatch.addPreference(preferenceScreen)
|
||||||
|
}
|
||||||
|
}
|
@ -33,17 +33,6 @@ class SettingsResourcePatch : AbstractSettingsResourcePatch(
|
|||||||
override fun execute(context: ResourceContext): PatchResult {
|
override fun execute(context: ResourceContext): PatchResult {
|
||||||
super.execute(context)
|
super.execute(context)
|
||||||
|
|
||||||
/*
|
|
||||||
* used for self-restart
|
|
||||||
*/
|
|
||||||
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
|
||||||
editor.file.getElementsByTagName("manifest").item(0).also {
|
|
||||||
it.appendChild(it.ownerDocument.createElement("uses-permission").also { element ->
|
|
||||||
element.setAttribute("android:name", "android.permission.SCHEDULE_EXACT_ALARM")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* used by a fingerprint of SettingsPatch
|
* used by a fingerprint of SettingsPatch
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
</PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user