diff --git a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt index 5bdaa5abb..cf5b0b816 100644 --- a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt +++ b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/ArrayResource.kt @@ -19,8 +19,10 @@ internal data class ArrayResource( override fun serialize(ownerDocument: Document, resourceCallback: ((IResource) -> Unit)?): Element { return super.serialize(ownerDocument, resourceCallback).apply { + setAttribute("name", name) + items.forEach { item -> - setAttribute("name", item.also { resourceCallback?.invoke(it) }.name) + resourceCallback?.invoke(item) this.appendChild(ownerDocument.createElement("item").also { itemNode -> itemNode.textContent = item.value diff --git a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceCategory.kt b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceCategory.kt index e37710a1e..f575dfe3a 100644 --- a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceCategory.kt +++ b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceCategory.kt @@ -15,7 +15,7 @@ import org.w3c.dom.Element internal open class PreferenceCategory( key: String, title: StringResource, - val preferences: List + var preferences: List ) : BasePreference(key, title) { override val tag: String = "PreferenceCategory" diff --git a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceScreen.kt b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceScreen.kt index 462be9e02..b7c82ba6f 100644 --- a/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceScreen.kt +++ b/src/main/kotlin/app/revanced/patches/shared/settings/preference/impl/PreferenceScreen.kt @@ -17,7 +17,7 @@ import org.w3c.dom.Element internal open class PreferenceScreen( key: String, title: StringResource, - val preferences: List, + var preferences: List, val summary: StringResource? = null ) : BasePreference(key, title) { override val tag: String = "PreferenceScreen" diff --git a/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt index cf7c5bbda..c8197a1a1 100644 --- a/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt +++ b/src/main/kotlin/app/revanced/patches/shared/settings/resource/patch/AbstractSettingsResourcePatch.kt @@ -26,6 +26,17 @@ abstract class AbstractSettingsResourcePatch( private val sourceDirectory: String, ) : ResourcePatch { 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 */ context.copyResources( sourceDirectory, diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/annotations/SettingsCompatibility.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/annotations/SettingsCompatibility.kt new file mode 100644 index 000000000..0eadf6909 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/annotations/SettingsCompatibility.kt @@ -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 diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt new file mode 100644 index 000000000..e2d0e51e5 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/bytecode/patch/SettingsPatch.kt @@ -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->(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() +} diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/components/CustomPreferenceCategory.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/components/CustomPreferenceCategory.kt new file mode 100644 index 000000000..20c7bdb80 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/components/CustomPreferenceCategory.kt @@ -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 +) : PreferenceCategory(key, title, preferences) { + override val tag: String = "app.revanced.twitch.settingsmenu.preference.CustomPreferenceCategory" +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/MenuGroupsOnClickFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/MenuGroupsOnClickFingerprint.kt new file mode 100644 index 000000000..1a6375986 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/MenuGroupsOnClickFingerprint.kt @@ -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") + } +) diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/MenuGroupsUpdatedFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/MenuGroupsUpdatedFingerprint.kt new file mode 100644 index 000000000..913f31d14 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/MenuGroupsUpdatedFingerprint.kt @@ -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 == "" + } +) diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/SettingsActivityOnCreateFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/SettingsActivityOnCreateFingerprint.kt new file mode 100644 index 000000000..f976f0210 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/SettingsActivityOnCreateFingerprint.kt @@ -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" + } +) diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/SettingsMenuItemEnumFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/SettingsMenuItemEnumFingerprint.kt new file mode 100644 index 000000000..cdb2dd177 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/fingerprints/SettingsMenuItemEnumFingerprint.kt @@ -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 == "" + } +) diff --git a/src/main/kotlin/app/revanced/patches/twitch/misc/settings/resource/patch/SettingsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/resource/patch/SettingsResourcePatch.kt new file mode 100644 index 000000000..07420cea9 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitch/misc/settings/resource/patch/SettingsResourcePatch.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/settings/resource/patch/SettingsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/settings/resource/patch/SettingsResourcePatch.kt index 7bad39653..2bd580d34 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/misc/settings/resource/patch/SettingsResourcePatch.kt +++ b/src/main/kotlin/app/revanced/patches/youtube/misc/settings/resource/patch/SettingsResourcePatch.kt @@ -33,17 +33,6 @@ class SettingsResourcePatch : AbstractSettingsResourcePatch( override fun execute(context: ResourceContext): PatchResult { 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 */ diff --git a/src/main/resources/twitch/settings/xml/revanced_prefs.xml b/src/main/resources/twitch/settings/xml/revanced_prefs.xml new file mode 100644 index 000000000..14e1c85de --- /dev/null +++ b/src/main/resources/twitch/settings/xml/revanced_prefs.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file