├── local.properties ├── extensions ├── shared │ ├── src │ │ └── main │ │ │ └── AndroidManifest.xml │ ├── build.gradle.kts │ └── library │ │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── app │ │ │ └── revanced │ │ │ └── extension │ │ │ └── shared │ │ │ ├── fixes │ │ │ └── slink │ │ │ │ ├── ResolveResult.java │ │ │ │ └── BaseFixSLinksPatch.java │ │ │ ├── checks │ │ │ ├── PatchInfo.java │ │ │ └── Check.java │ │ │ ├── requests │ │ │ ├── Route.java │ │ │ └── Requester.java │ │ │ ├── settings │ │ │ ├── AppLanguage.java │ │ │ ├── BaseSettings.java │ │ │ ├── preference │ │ │ │ ├── ResettableEditTextPreference.java │ │ │ │ ├── ImportExportPreference.java │ │ │ │ └── SharedPrefCategory.java │ │ │ ├── LongSetting.java │ │ │ ├── StringSetting.java │ │ │ ├── FloatSetting.java │ │ │ ├── IntegerSetting.java │ │ │ ├── BooleanSetting.java │ │ │ └── EnumSetting.java │ │ │ ├── spoof │ │ │ ├── requests │ │ │ │ └── PlayerRoutes.java │ │ │ └── ClientType.java │ │ │ ├── StringRef.java │ │ │ └── Logger.java │ │ └── build.gradle.kts ├── instagram │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── app │ │ │ └── revanced │ │ │ └── extension │ │ │ └── ShareLinkExtension.java │ ├── stub │ │ ├── src │ │ │ └── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── instagram │ │ │ │ ├── user │ │ │ │ └── model │ │ │ │ │ └── User.java │ │ │ │ └── model │ │ │ │ └── mediasize │ │ │ │ └── ProfilePicUrlInfoImpl.java │ │ └── build.gradle.kts │ └── build.gradle.kts └── proguard-rules.pro ├── .editorconfig ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── gradle.properties ├── patches ├── src │ └── main │ │ └── kotlin │ │ └── li │ │ └── auna │ │ ├── patches │ │ ├── shared │ │ │ ├── misc │ │ │ │ ├── settings │ │ │ │ │ ├── preference │ │ │ │ │ │ ├── SummaryType.kt │ │ │ │ │ │ ├── InputType.kt │ │ │ │ │ │ ├── PreferenceCategory.kt │ │ │ │ │ │ ├── TextPreference.kt │ │ │ │ │ │ ├── SwitchPreference.kt │ │ │ │ │ │ ├── NonInteractivePreference.kt │ │ │ │ │ │ ├── IntentPreference.kt │ │ │ │ │ │ ├── BasePreference.kt │ │ │ │ │ │ ├── PreferenceScreenPreference.kt │ │ │ │ │ │ ├── ListPreference.kt │ │ │ │ │ │ └── BasePreferenceScreen.kt │ │ │ │ │ └── SettingsPatch.kt │ │ │ │ ├── checks │ │ │ │ │ ├── Fingerprints.kt │ │ │ │ │ └── BaseCheckEnvironmentPatch.kt │ │ │ │ ├── extension │ │ │ │ │ ├── Fingerprints.kt │ │ │ │ │ └── SharedExtensionPatch.kt │ │ │ │ ├── fix │ │ │ │ │ └── verticalscroll │ │ │ │ │ │ ├── Fingerprints.kt │ │ │ │ │ │ └── VerticalScrollPatch.kt │ │ │ │ ├── gms │ │ │ │ │ └── Fingerprints.kt │ │ │ │ ├── mapping │ │ │ │ │ └── ResourceMappingPatch.kt │ │ │ │ └── hex │ │ │ │ │ └── HexPatch.kt │ │ │ └── Fingerprints.kt │ │ ├── instagram │ │ │ ├── misc │ │ │ │ ├── extension │ │ │ │ │ └── ExtensionPatch.kt │ │ │ │ ├── bypassintegrity │ │ │ │ │ ├── BypassIntegrityPatch.kt │ │ │ │ │ └── Fingerprints.kt │ │ │ │ └── quality │ │ │ │ │ ├── Fingerprints.kt │ │ │ │ │ └── MaxMediaQualityPatch.kt │ │ │ ├── interaction │ │ │ │ └── bio │ │ │ │ │ ├── Fingerprints.kt │ │ │ │ │ └── SelectableBioPatch.kt │ │ │ ├── layout │ │ │ │ ├── Fingerprints.kt │ │ │ │ └── EnableDeveloperMenuPatch.kt │ │ │ └── adblock │ │ │ │ ├── Fingerprints.kt │ │ │ │ └── AdBlockPatch.kt │ │ ├── telegram │ │ │ ├── ads │ │ │ │ ├── Fingerprints.kt │ │ │ │ └── DisableAdsPatch.kt │ │ │ ├── downloadboost │ │ │ │ ├── Fingerprints.kt │ │ │ │ └── DownloadBoostPatch.kt │ │ │ ├── typingindicator │ │ │ │ ├── Fingerprints.kt │ │ │ │ └── HideTypingIndicatorPatch.kt │ │ │ ├── bypassintegrity │ │ │ │ ├── Fingerprints.kt │ │ │ │ └── BypassIntegrityPatch.kt │ │ │ ├── pro │ │ │ │ ├── UnlockPro.kt │ │ │ │ └── Fingerprints.kt │ │ │ └── disableautoupdate │ │ │ │ ├── DisableAutoUpdate.kt │ │ │ │ └── Fingerprints.kt │ │ ├── kustom │ │ │ ├── Fingerprints.kt │ │ │ └── UnlockProPatch.kt │ │ ├── youtube │ │ │ └── layout │ │ │ │ ├── hidesubtitletoast │ │ │ │ ├── HideSubtitleToastPatch.kt │ │ │ │ └── Fingerprints.kt │ │ │ │ └── largepausebutton │ │ │ │ └── LargePauseButtonPatch.kt │ │ └── all │ │ │ └── misc │ │ │ └── packagename │ │ │ └── ChangePackageNamePatch.kt │ │ └── util │ │ ├── Utils.kt │ │ ├── resource │ │ ├── ArrayResource.kt │ │ ├── BaseResource.kt │ │ └── StringResource.kt │ │ └── ResourceUtils.kt ├── stub │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── android │ │ └── os │ │ └── Build.java └── build.gradle.kts ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── dependabot.yml └── workflows │ ├── open_pull_request.yml │ ├── build_pull_request.yml │ └── release.yml ├── package.json ├── settings.gradle.kts ├── .releaserc ├── README.md ├── assets └── revanced-logo │ └── revanced-logo.svg ├── .gitignore ├── gradlew.bat └── CONTRIBUTING.md /local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir = /home/aun/Android/Sdk -------------------------------------------------------------------------------- /extensions/shared/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /extensions/instagram/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /extensions/instagram/stub/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_code_style = intellij_idea 3 | ktlint_standard_no-wildcard-imports = disabled -------------------------------------------------------------------------------- /extensions/shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(project(":extensions:shared:library")) 3 | } 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aunali321/ReVancedExperiments/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /extensions/instagram/stub/src/main/java/com/instagram/user/model/User.java: -------------------------------------------------------------------------------- 1 | package com.instagram.user.model; 2 | 3 | public class User { 4 | 5 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel = true 2 | org.gradle.caching = true 3 | android.useAndroidX = true 4 | kotlin.code.style = official 5 | version = 1.9.0 6 | -------------------------------------------------------------------------------- /extensions/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -dontoptimize 3 | -keepattributes * 4 | -keep class app.revanced.** { 5 | *; 6 | } 7 | -keep class com.google.** { 8 | *; 9 | } -------------------------------------------------------------------------------- /extensions/instagram/stub/src/main/java/com/instagram/model/mediasize/ProfilePicUrlInfoImpl.java: -------------------------------------------------------------------------------- 1 | package com.instagram.model.mediasize; 2 | 3 | public class ProfilePicUrlInfoImpl { 4 | 5 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/SummaryType.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | enum class SummaryType(val type: String) { 4 | DEFAULT("summary"), ON("summaryOn"), OFF("summaryOff") 5 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/misc/extension/ExtensionPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.misc.extension 2 | 3 | import li.auna.patches.shared.misc.extension.sharedExtensionPatch 4 | 5 | val sharedExtensionPatch = sharedExtensionPatch("instagram") -------------------------------------------------------------------------------- /patches/stub/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | description = "Provide Android API stubs for ReVanced Patches." 6 | 7 | java { 8 | sourceCompatibility = JavaVersion.VERSION_11 9 | targetCompatibility = JavaVersion.VERSION_11 10 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🗨 Discussions 4 | url: https://github.com/revanced/revanced-suggestions/discussions 5 | about: Have something unspecific to ReVanced Patches template in mind? Search for or start a new discussion! -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/interaction/bio/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.interaction.bio 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val selectableBioFingerprint = fingerprint { 6 | returns("V") 7 | strings("is_bio_visible") 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@saithodev/semantic-release-backmerge": "^4.0.1", 4 | "@semantic-release/changelog": "^6.0.3", 5 | "@semantic-release/git": "^10.0.1", 6 | "gradle-semantic-release-plugin": "^1.10.1", 7 | "semantic-release": "^24.1.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /extensions/instagram/build.gradle.kts: -------------------------------------------------------------------------------- 1 | extension { 2 | name = "extensions/extension.rve" 3 | } 4 | 5 | android { 6 | namespace = "app.revanced.extension" 7 | } 8 | 9 | dependencies { 10 | compileOnly(project(":extensions:shared:library")) 11 | compileOnly(project(":extensions:instagram:stub")) 12 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/InputType.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | enum class InputType(val type: String) { 4 | TEXT("text"), 5 | TEXT_CAP_CHARACTERS("textCapCharacters"), 6 | TEXT_MULTI_LINE("textMultiLine"), 7 | NUMBER("number"), 8 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package li.auna.util 2 | 3 | internal object Utils { 4 | internal fun String.trimIndentMultiline() = 5 | this.split("\n") 6 | .joinToString("\n") { it.trimIndent() } // Remove the leading whitespace from each line. 7 | .trimIndent() // Remove the leading newline. 8 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/layout/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.layout 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val shouldAddPrefTTLFingerprint = fingerprint { 6 | custom { methodDef, classDef -> 7 | methodDef.name == "shouldAddPrefTTL" && classDef.type.endsWith("Lcom/instagram/debug/whoptions/WhitehatOptionsFragment;") 8 | } 9 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val castContextFetchFingerprint = fingerprint { 6 | strings("Error fetching CastContext.") 7 | } 8 | 9 | internal val primeMethodFingerprint = fingerprint { 10 | strings("com.google.android.GoogleCamera", "com.android.vending") 11 | } 12 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.fixes.slink; 2 | 3 | public enum ResolveResult { 4 | // Let app handle rest of stuff 5 | CONTINUE, 6 | // Start app, to make it cache its access_token 7 | ACCESS_TOKEN_START, 8 | // Don't do anything - we started resolving 9 | DO_NOTHING 10 | } 11 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/ads/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.ads 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val hideSponsoredMessagesFingerprint = fingerprint { 6 | returns("V") 7 | custom { methodDef, classDef -> 8 | methodDef.name == "addSponsoredMessages" && classDef.type.endsWith("Lorg/telegram/ui/ChatActivity;") 9 | } 10 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/downloadboost/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.downloadboost 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val updateParamsFingerprint = fingerprint { 6 | returns("V") 7 | custom { methodDef, classDef -> 8 | methodDef.name == "updateParams" && classDef.type.endsWith("Lorg/telegram/messenger/FileLoadOperation;") 9 | } 10 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | revanced-patcher = "21.0.0" 3 | # Tracking https://github.com/google/smali/issues/64. 4 | #noinspection GradleDependency 5 | smali = "3.0.5" 6 | annotation = "1.9.1" 7 | agp = "8.2.2" 8 | 9 | [libraries] 10 | annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } 11 | 12 | [plugins] 13 | android-library = { id = "com.android.library", version.ref = "agp" } 14 | -------------------------------------------------------------------------------- /extensions/shared/library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | } 4 | 5 | android { 6 | namespace = "app.revanced.extension" 7 | compileSdk = 34 8 | 9 | defaultConfig { 10 | minSdk = 23 11 | } 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_17 15 | targetCompatibility = JavaVersion.VERSION_17 16 | } 17 | } 18 | 19 | dependencies { 20 | compileOnly(libs.annotation) 21 | } 22 | -------------------------------------------------------------------------------- /extensions/instagram/stub/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(libs.plugins.android.library.get().pluginId) 3 | } 4 | 5 | android { 6 | namespace = "app.revanced.extension" 7 | compileSdk = 33 8 | 9 | defaultConfig { 10 | minSdk = 24 11 | } 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | } 18 | 19 | dependencies { 20 | compileOnly(libs.annotation) 21 | } 22 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/typingindicator/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.typingindicator 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.Opcode 5 | 6 | internal val needSendTypingFingerprint = fingerprint { 7 | returns("V") 8 | custom { methodDef, _ -> 9 | methodDef.name == "needSendTyping" 10 | } 11 | opcodes( 12 | Opcode.IGET_OBJECT, 13 | Opcode.INVOKE_STATIC 14 | ) 15 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/adblock/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.adblock 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | 6 | internal val adInjectorFingerprint = fingerprint { 7 | accessFlags(AccessFlags.PRIVATE) 8 | returns("Z") 9 | parameters("L", "L") 10 | strings( 11 | "SponsoredContentController.insertItem", 12 | "SponsoredContentController::Delivery", 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/checks/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.checks 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val patchInfoFingerprint = fingerprint { 6 | custom { _, classDef -> classDef.type == "Lapp/revanced/extension/shared/checks/PatchInfo;" } 7 | } 8 | 9 | internal val patchInfoBuildFingerprint = fingerprint { 10 | custom { _, classDef -> classDef.type == "Lapp/revanced/extension/shared/checks/PatchInfo\$Build;" } 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | labels: [] 5 | directory: / 6 | target-branch: dev 7 | schedule: 8 | interval: monthly 9 | 10 | - package-ecosystem: npm 11 | labels: [] 12 | directory: / 13 | target-branch: dev 14 | schedule: 15 | interval: monthly 16 | 17 | - package-ecosystem: gradle 18 | labels: [] 19 | directory: / 20 | target-branch: dev 21 | schedule: 22 | interval: monthly 23 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/extension/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.extension 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | 6 | internal val revancedUtilsPatchesVersionFingerprint = fingerprint { 7 | accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) 8 | returns("Ljava/lang/String;") 9 | parameters() 10 | custom { method, _ -> 11 | method.name == "getPatchesReleaseVersion" && method.definingClass == EXTENSION_CLASS_DESCRIPTOR 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/ads/DisableAdsPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.ads 2 | 3 | import app.revanced.patcher.patch.bytecodePatch 4 | import li.auna.util.returnEarly 5 | 6 | @Suppress("unused") 7 | val hideAdsPatch = bytecodePatch( 8 | name = "Hide sponsored ads", 9 | description = "Hides sponsored ads in channels", 10 | ) { 11 | compatibleWith( 12 | "org.telegram.messenger", 13 | "org.telegram.messenger.web", 14 | "uz.unnarsx.cherrygram" 15 | ) 16 | 17 | execute { 18 | hideSponsoredMessagesFingerprint.method.returnEarly() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /patches/build.gradle.kts: -------------------------------------------------------------------------------- 1 | group = "li.auna" 2 | 3 | patches { 4 | about { 5 | name = "ReVanced Experiments" 6 | description = "A collection of patches for ReVanced" 7 | source = "git@github.com:Aunali321/ReVancedExperiments.git" 8 | author = "AunAli" 9 | contact = "hello@auna.li" 10 | website = "https://auna.li" 11 | license = "GNU General Public License v3.0" 12 | } 13 | } 14 | 15 | dependencies { 16 | compileOnly(project(":patches:stub")) 17 | } 18 | 19 | kotlin { 20 | compilerOptions { 21 | freeCompilerArgs = listOf("-Xcontext-receivers") 22 | } 23 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/adblock/AdBlockPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.adblock 2 | 3 | import app.revanced.patcher.patch.bytecodePatch 4 | import li.auna.util.returnEarly 5 | 6 | @Suppress("unused") 7 | val adBlockPatch = bytecodePatch( 8 | name = "Hide Ads", 9 | description = "Hides ads in stories, discover, profile, etc. " + 10 | "An ad can still appear once when refreshing the home feed.", 11 | ) { 12 | compatibleWith( 13 | "com.instagram.android", 14 | "com.instagram.barcelona", 15 | ) 16 | 17 | execute { 18 | adInjectorFingerprint.method.returnEarly(false) 19 | } 20 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/typingindicator/HideTypingIndicatorPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.typingindicator 2 | 3 | import app.revanced.patcher.patch.bytecodePatch 4 | import li.auna.util.returnEarly 5 | 6 | @Suppress("unused") 7 | val hideAdsPatch = bytecodePatch( 8 | name = "Hide typing indicator", 9 | description = "Hides your typing indicator from other users", 10 | ) { 11 | compatibleWith( 12 | "org.telegram.messenger", 13 | "org.telegram.messenger.web", 14 | "uz.unnarsx.cherrygram" 15 | ) 16 | 17 | execute { 18 | needSendTypingFingerprint.method.returnEarly() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/bypassintegrity/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.bypassintegrity 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | 6 | internal val bypassIntegrityFingerprint = fingerprint { 7 | accessFlags(AccessFlags.PRIVATE, AccessFlags.SYNTHETIC) 8 | returns("V") 9 | strings("basicIntegrity", "ctsProfileMatch") 10 | } 11 | 12 | internal val spoofSignatureFingerprint = fingerprint { 13 | custom { methodDef, classDef -> 14 | methodDef.name == "getCertificateSHA256Fingerprint" && classDef.type.endsWith("Lorg/telegram/messenger/AndroidUtilities;") 15 | } 16 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/fix/verticalscroll/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.fix.verticalscroll 2 | 3 | import com.android.tools.smali.dexlib2.Opcode 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | import app.revanced.patcher.fingerprint 6 | 7 | internal val canScrollVerticallyFingerprint = fingerprint { 8 | accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) 9 | returns("Z") 10 | parameters() 11 | opcodes( 12 | Opcode.MOVE_RESULT, 13 | Opcode.RETURN, 14 | Opcode.INVOKE_VIRTUAL, 15 | Opcode.MOVE_RESULT 16 | ) 17 | custom { _, classDef -> classDef.endsWith("SwipeRefreshLayout;") } 18 | } -------------------------------------------------------------------------------- /.github/workflows/open_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Open a PR to main 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | workflow_dispatch: 8 | 9 | env: 10 | MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main` 11 | 12 | jobs: 13 | pull-request: 14 | name: Open pull request 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Open pull request 21 | uses: repo-sync/pull-request@v2 22 | with: 23 | destination_branch: 'main' 24 | pr_title: 'chore: ${{ env.MESSAGE }}' 25 | pr_body: 'This pull request will ${{ env.MESSAGE }}.' 26 | pr_draft: true 27 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/misc/bypassintegrity/BypassIntegrityPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.misc.bypassintegrity 2 | 3 | import app.revanced.patcher.patch.bytecodePatch 4 | import li.auna.util.returnEarly 5 | 6 | @Suppress("unused") 7 | val signatureCheckPatch = bytecodePatch( 8 | name = "Disable signature check", 9 | description = "Disables the signature check that causes the app to crash on startup." 10 | ) { 11 | compatibleWith("com.instagram.android"("378.0.0.52.68")) 12 | 13 | execute { 14 | isValidSignatureMethodFingerprint 15 | .match(isValidSignatureClassFingerprint.classDef) 16 | .method 17 | .returnEarly(true) 18 | } 19 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/pro/UnlockPro.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.pro 2 | 3 | import app.revanced.patcher.patch.bytecodePatch 4 | import li.auna.util.returnEarly 5 | 6 | @Suppress("unused") 7 | val unlockProPatch = bytecodePatch( 8 | name = "Unlock Pro", 9 | description = "Unlock client-side Pro features", 10 | ) { 11 | compatibleWith( 12 | "org.telegram.messenger", 13 | "org.telegram.messenger.web", 14 | "uz.unnarsx.cherrygram" 15 | ) 16 | 17 | execute { 18 | setOf( 19 | isPremiumUserFingerprint, 20 | isPremiumFingerprint, 21 | isPremiumForStoryFingerprint 22 | ).forEach { it.method.returnEarly(true) } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/misc/bypassintegrity/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.misc.bypassintegrity 2 | 3 | import app.revanced.patcher.fingerprint 4 | import li.auna.util.getReference 5 | import li.auna.util.indexOfFirstInstruction 6 | import com.android.tools.smali.dexlib2.iface.reference.MethodReference 7 | 8 | internal val isValidSignatureClassFingerprint = fingerprint { 9 | strings("The provider for uri '", "' is not trusted: ") 10 | } 11 | 12 | internal val isValidSignatureMethodFingerprint = fingerprint { 13 | parameters("L", "Z") 14 | returns("Z") 15 | custom { method, _ -> 16 | method.indexOfFirstInstruction { 17 | getReference()?.name == "keySet" 18 | } >= 0 19 | } 20 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/misc/quality/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.misc.quality 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val displayMetricsFingerprint = fingerprint { 6 | returns("Ljava/lang/String;") 7 | strings("%sdpi; %sx%s") 8 | } 9 | 10 | internal val mediaSizeFingerprint = fingerprint { 11 | strings("_8.jpg", "_6.jpg") 12 | } 13 | 14 | internal val storyMediaBitrateFingerprint = fingerprint { 15 | returns("Landroid/media/MediaFormat;") 16 | strings("color-format", "bitrate", "frame-rate", "i-frame-interval", "profile", "level") 17 | } 18 | 19 | internal val videoEncoderConfigFingerprint = fingerprint { 20 | returns("Ljava/lang/String;") 21 | strings("VideoEncoderConfig{width=") 22 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/kustom/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.kustom 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | 6 | internal val hasPurchasedFingerprint = fingerprint { 7 | accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) 8 | returns("Z") 9 | custom { methodDef, classDef -> 10 | methodDef.name == "isLicensed" && classDef.type.endsWith("Lorg/kustom/billing/LicenseState;") 11 | } 12 | } 13 | 14 | internal val isPurchaseValidFingerprint = fingerprint { 15 | accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) 16 | returns("Z") 17 | custom { methodDef, classDef -> 18 | methodDef.name == "isValid" && classDef.type.endsWith("Lorg/kustom/billing/LicenseState;") 19 | } 20 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/disableautoupdate/DisableAutoUpdate.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.disableautoupdate 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstruction 4 | import app.revanced.patcher.patch.bytecodePatch 5 | import li.auna.util.returnEarly 6 | 7 | @Suppress("unused") 8 | val unlockProPatch = bytecodePatch( 9 | name = "Disable Auto Update", 10 | description = "Disable Auto Update", 11 | ) { 12 | compatibleWith( 13 | "org.telegram.messenger", "org.telegram.messenger.web", "uz.unnarsx.cherrygram" 14 | ) 15 | 16 | execute { 17 | checkAppUpdateFingerprint.method.returnEarly(false) 18 | setNewAppVersionAvailableFingerprint.method.returnEarly(false) 19 | blockViewUpdateFingerprint.method.returnEarly() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "revanced-experiments-patches" 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | google() 7 | maven { 8 | name = "GitHubPackages" 9 | url = uri("https://maven.pkg.github.com/revanced/registry") 10 | credentials { 11 | username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") 12 | password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") 13 | } 14 | } 15 | } 16 | } 17 | 18 | plugins { 19 | id("app.revanced.patches") version "1.0.0-dev.7" 20 | } 21 | 22 | settings { 23 | extensions { 24 | defaultNamespace = "app.revanced.extension" 25 | 26 | proguardFiles("../proguard-rules.pro") 27 | } 28 | } 29 | 30 | include(":patches:stub") -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/pro/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.pro 2 | 3 | import app.revanced.patcher.fingerprint 4 | 5 | internal val isPremiumUserFingerprint = fingerprint { 6 | returns("Z") 7 | custom { methodDef, classDef -> 8 | methodDef.name == "isPremiumUser" && classDef.type.endsWith("Lorg/telegram/messenger/MessagesController;") 9 | 10 | } 11 | } 12 | 13 | internal val isPremiumFingerprint = fingerprint { 14 | returns("Z") 15 | custom { methodDef, classDef -> 16 | methodDef.name == "isPremium" && classDef.type.endsWith("Lorg/telegram/messenger/UserConfig;") 17 | } 18 | } 19 | 20 | internal val isPremiumForStoryFingerprint = fingerprint { 21 | returns("Z") 22 | custom { methodDef, classDef -> 23 | methodDef.name == "isPremium" && classDef.type.endsWith("Lorg/telegram/ui/Stories/StoriesController;") 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /.github/workflows/build_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Build pull request 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | 9 | jobs: 10 | release: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Java 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: "temurin" 23 | java-version: "17" 24 | 25 | - name: Cache Gradle 26 | uses: burrunan/gradle-cache-action@v2 27 | 28 | - name: Build 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | run: ./gradlew build --no-daemon 32 | 33 | - name: Upload artifacts 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: revanced-patches 37 | path: patches/build/libs 38 | -------------------------------------------------------------------------------- /patches/stub/src/main/java/android/os/Build.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public class Build { 4 | public static final String BOARD = null; 5 | public static final String BOOTLOADER = null; 6 | public static final String BRAND = null; 7 | public static final String CPU_ABI = null; 8 | public static final String CPU_ABI2 = null; 9 | public static final String DEVICE = null; 10 | public static final String DISPLAY = null; 11 | public static final String FINGERPRINT = null; 12 | public static final String HARDWARE = null; 13 | public static final String HOST = null; 14 | public static final String ID = null; 15 | public static final String MANUFACTURER = null; 16 | public static final String MODEL = null; 17 | public static final String PRODUCT = null; 18 | public static final String RADIO = null; 19 | public static final String TAGS = null; 20 | public static final String TYPE = null; 21 | public static final String USER = null; 22 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/disableautoupdate/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.disableautoupdate 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.Opcode 5 | 6 | internal val checkAppUpdateFingerprint = fingerprint { 7 | returns("V") 8 | custom { methodDef, classDef -> 9 | methodDef.name == "checkAppUpdate" && classDef.type.endsWith("Lorg/telegram/ui/LaunchActivity;") 10 | } 11 | } 12 | 13 | internal val setNewAppVersionAvailableFingerprint = fingerprint { 14 | returns("Z") 15 | custom { methodDef, classDef -> 16 | methodDef.name == "setNewAppVersionAvailable" && classDef.type.endsWith("Lorg/telegram/messenger/SharedConfig;") 17 | } 18 | } 19 | 20 | internal val blockViewUpdateFingerprint = fingerprint { 21 | returns("V") 22 | custom { methodDef, classDef -> 23 | methodDef.name == "show" && classDef.type.endsWith("Lorg/telegram/ui/Components/BlockingUpdateView;") 24 | } 25 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/gms/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.gms 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | 6 | const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId" 7 | 8 | internal val gmsCoreSupportFingerprint = fingerprint { 9 | custom { _, classDef -> 10 | classDef.endsWith("GmsCoreSupport;") 11 | } 12 | } 13 | 14 | internal val googlePlayUtilityFingerprint = fingerprint { 15 | accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) 16 | returns("I") 17 | parameters("L", "I") 18 | strings( 19 | "This should never happen.", 20 | "MetadataValueReader", 21 | "com.google.android.gms", 22 | ) 23 | } 24 | 25 | internal val serviceCheckFingerprint = fingerprint { 26 | accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) 27 | returns("V") 28 | parameters("L", "I") 29 | strings("Google Play Services not available") 30 | } 31 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/youtube/layout/hidesubtitletoast/HideSubtitleToastPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.youtube.layout.hidesubtitletoast 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.instructions 4 | import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction 5 | import app.revanced.patcher.patch.bytecodePatch 6 | import li.auna.util.returnEarly 7 | 8 | @Suppress("unused") 9 | val hideSubtitleToastPatch = bytecodePatch( 10 | name = "Hide subtitle toast", 11 | description = "Hides the subtitle toast when toggling subtitles on/off and when changing subtitles", 12 | ) { 13 | compatibleWith( 14 | "com.google.android.youtube", 15 | "app.revanced.android.youtube" 16 | ) 17 | 18 | execute { 19 | hideSubtitleToastFingerprint.method.returnEarly() 20 | 21 | hideSubtitleToastFingerprint2.method.apply { 22 | val lastIndex = instructions.lastIndex - 2 23 | removeInstruction(lastIndex) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/kustom/UnlockProPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.kustom 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.instructions 4 | import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction 5 | import app.revanced.patcher.patch.bytecodePatch 6 | 7 | @Suppress("unused") 8 | val hideSubtitleToastPatch = bytecodePatch( 9 | name = "Unlock Pro", 10 | description = "Unlock Pro features", 11 | ) { 12 | compatibleWith( 13 | "org.kustom.wallpaper"("3.73b314511"), 14 | "org.kustom.widget"("3.73b314511"), 15 | "org.kustom.lockscreen"("3.73b314511"), 16 | ) 17 | 18 | execute { 19 | hasPurchasedFingerprint.method.apply { 20 | val index = instructions.lastIndex - 1 21 | // Set hasPremium = true. 22 | replaceInstruction(index, "const/4 v0, 0x1") 23 | } 24 | 25 | isPurchaseValidFingerprint.method.apply { 26 | val index = instructions.lastIndex - 3 27 | replaceInstruction(index, "const/4 v0, 0x1") 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.fix.verticalscroll 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstruction 4 | import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 5 | import app.revanced.patcher.patch.bytecodePatch 6 | import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction 7 | 8 | val verticalScrollPatch = bytecodePatch( 9 | description = "Fixes issues with refreshing the feed when the first component is of type EmptyComponent.", 10 | ) { 11 | 12 | execute { 13 | canScrollVerticallyFingerprint.method.apply { 14 | val moveResultIndex = canScrollVerticallyFingerprint.patternMatch!!.endIndex 15 | val moveResultRegister = getInstruction(moveResultIndex).registerA 16 | 17 | val insertIndex = moveResultIndex + 1 18 | addInstruction( 19 | insertIndex, 20 | "const/4 v$moveResultRegister, 0x0", 21 | ) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/PreferenceCategory.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | 6 | /** 7 | * A preference category. 8 | * 9 | * @param key The key of the preference. If null, other parameters must be specified. 10 | * @param titleKey The key of the preference title. 11 | * @param tag The tag or full class name of the preference. 12 | * @param preferences The preferences in this category. 13 | */ 14 | @Suppress("MemberVisibilityCanBePrivate") 15 | open class PreferenceCategory( 16 | key: String? = null, 17 | titleKey: String = "${key}_title", 18 | tag: String = "PreferenceCategory", 19 | val preferences: Set 20 | ) : BasePreference(key, titleKey, null, tag) { 21 | 22 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 23 | super.serialize(ownerDocument, resourceCallback).apply { 24 | preferences.forEach { 25 | appendChild(it.serialize(ownerDocument, resourceCallback)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/layout/EnableDeveloperMenuPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.layout 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 4 | import app.revanced.patcher.patch.bytecodePatch 5 | import com.android.tools.smali.dexlib2.Opcode 6 | 7 | import li.auna.util.indexOfFirstInstructionOrThrow 8 | 9 | @Suppress("unused") 10 | val enableDeveloperMenuPatch = bytecodePatch( 11 | name = "Enable Developer Menu", 12 | description = "Enables the developer menu.", 13 | ) { 14 | compatibleWith("com.instagram.android") 15 | 16 | execute { 17 | shouldAddPrefTTLFingerprint.method.apply { 18 | val isDeveloperMethodCallIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_STATIC } 19 | 20 | 21 | val isDeveloperMethod = navigate(this).to(isDeveloperMethodCallIndex).stop() 22 | 23 | // Enable the developer menu. 24 | isDeveloperMethod.addInstructions( 25 | 0, 26 | """ 27 | const v0, 0x1 28 | return v0 29 | """, 30 | ) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/TextPreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | 6 | /** 7 | * A text preference. 8 | * 9 | * @param key The preference key. If null, other parameters must be specified. 10 | * @param titleKey The preference title key. 11 | * @param summaryKey The preference summary key. 12 | * @param tag The preference tag. 13 | * @param inputType The preference input type. 14 | */ 15 | @Suppress("MemberVisibilityCanBePrivate") 16 | class TextPreference( 17 | key: String? = null, 18 | titleKey: String = "${key}_title", 19 | summaryKey: String? = "${key}_summary", 20 | tag: String = "app.revanced.extension.shared.settings.preference.ResettableEditTextPreference", 21 | val inputType: InputType = InputType.TEXT 22 | ) : BasePreference(key, titleKey, summaryKey, tag) { 23 | 24 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 25 | super.serialize(ownerDocument, resourceCallback).apply { 26 | setAttribute("android:inputType", inputType.type) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/youtube/layout/hidesubtitletoast/Fingerprints.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.youtube.layout.hidesubtitletoast 2 | 3 | import app.revanced.patcher.fingerprint 4 | import com.android.tools.smali.dexlib2.AccessFlags 5 | import com.android.tools.smali.dexlib2.Opcode 6 | 7 | internal val hideSubtitleToastFingerprint = fingerprint { 8 | accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) 9 | returns("V") 10 | parameters("Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;") 11 | opcodes( 12 | Opcode.APUT_OBJECT, 13 | Opcode.CHECK_CAST, 14 | Opcode.CONST, 15 | Opcode.INVOKE_VIRTUAL, 16 | Opcode.MOVE_RESULT_OBJECT, 17 | Opcode.GOTO 18 | ) 19 | } 20 | 21 | internal val hideSubtitleToastFingerprint2 = fingerprint { 22 | accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) 23 | returns("V") 24 | parameters("Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;") 25 | opcodes( 26 | Opcode.APUT_OBJECT, 27 | Opcode.CHECK_CAST, 28 | Opcode.CONST, 29 | Opcode.INVOKE_VIRTUAL, 30 | Opcode.MOVE_RESULT_OBJECT, 31 | Opcode.GOTO 32 | ) 33 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/SwitchPreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | 6 | /** 7 | * A switch preference. 8 | * 9 | * @param key The preference key. If null, other parameters must be specified. 10 | * @param titleKey The preference title key. 11 | * @param tag The preference tag. 12 | * @param summaryOnKey The preference summary-on key. 13 | * @param summaryOffKey The preference summary-off key. 14 | */ 15 | @Suppress("MemberVisibilityCanBePrivate") 16 | class SwitchPreference( 17 | key: String? = null, 18 | titleKey: String = "${key}_title", 19 | tag: String = "SwitchPreference", 20 | val summaryOnKey: String = "${key}_summary_on", 21 | val summaryOffKey: String = "${key}_summary_off" 22 | ) : BasePreference(key, titleKey, null, tag) { 23 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 24 | super.serialize(ownerDocument, resourceCallback).apply { 25 | addSummary(summaryOnKey, SummaryType.ON) 26 | addSummary(summaryOffKey, SummaryType.OFF) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/NonInteractivePreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | 6 | /** 7 | * A non-interactive preference. 8 | * 9 | * Typically used to present static text, but also used for custom extension code that responds to taps. 10 | * 11 | * @param key The preference key. 12 | * @param summaryKey The preference summary key. 13 | * @param tag The tag or full class name of the preference. 14 | * @param selectable If the preference is selectable and responds to tap events. 15 | */ 16 | @Suppress("MemberVisibilityCanBePrivate") 17 | class NonInteractivePreference( 18 | key: String, 19 | titleKey: String = "${key}_title", 20 | summaryKey: String? = "${key}_summary", 21 | tag: String = "Preference", 22 | val selectable: Boolean = false, 23 | ) : BasePreference(key, titleKey, summaryKey, tag) { 24 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 25 | super.serialize(ownerDocument, resourceCallback).apply { 26 | setAttribute("android:selectable", selectable.toString()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.checks; 2 | 3 | /** 4 | * Fields are set by the patch. Do not modify. 5 | * Fields are not final, because the compiler is inlining them. 6 | * 7 | * @noinspection CanBeFinal 8 | */ 9 | final class PatchInfo { 10 | static long PATCH_TIME = 0L; 11 | 12 | final static class Build { 13 | static String PATCH_BOARD = ""; 14 | static String PATCH_BOOTLOADER = ""; 15 | static String PATCH_BRAND = ""; 16 | static String PATCH_CPU_ABI = ""; 17 | static String PATCH_CPU_ABI2 = ""; 18 | static String PATCH_DEVICE = ""; 19 | static String PATCH_DISPLAY = ""; 20 | static String PATCH_FINGERPRINT = ""; 21 | static String PATCH_HARDWARE = ""; 22 | static String PATCH_HOST = ""; 23 | static String PATCH_ID = ""; 24 | static String PATCH_MANUFACTURER = ""; 25 | static String PATCH_MODEL = ""; 26 | static String PATCH_PRODUCT = ""; 27 | static String PATCH_RADIO = ""; 28 | static String PATCH_TAGS = ""; 29 | static String PATCH_TYPE = ""; 30 | static String PATCH_USER = ""; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | { 5 | "name": "dev", 6 | "prerelease": true 7 | } 8 | ], 9 | "plugins": [ 10 | [ 11 | "@semantic-release/commit-analyzer", { 12 | "releaseRules": [ 13 | { "type": "build", "scope": "Needs bump", "release": "patch" } 14 | ] 15 | } 16 | ], 17 | "@semantic-release/release-notes-generator", 18 | "@semantic-release/changelog", 19 | "gradle-semantic-release-plugin", 20 | [ 21 | "@semantic-release/git", 22 | { 23 | "assets": [ 24 | "README.md", 25 | "CHANGELOG.md", 26 | "gradle.properties", 27 | ], 28 | "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 29 | } 30 | ], 31 | [ 32 | "@semantic-release/github", 33 | { 34 | "assets": [ 35 | { 36 | "path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)" 37 | } 38 | ], 39 | successComment: false 40 | } 41 | ], 42 | [ 43 | "@saithodev/semantic-release-backmerge", 44 | { 45 | backmergeBranches: [{"from": "main", "to": "dev"}], 46 | clearWorkspace: true 47 | } 48 | ] 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/util/resource/ArrayResource.kt: -------------------------------------------------------------------------------- 1 | package li.auna.util.resource 2 | 3 | import li.auna.util.childElementsSequence 4 | import org.w3c.dom.Document 5 | import org.w3c.dom.Node 6 | 7 | /** 8 | * An array resource. 9 | * 10 | * @param name The name of the array resource. 11 | * @param items The items of the array resource. 12 | */ 13 | @Suppress("MemberVisibilityCanBePrivate") 14 | class ArrayResource( 15 | name: String, 16 | val items: List, 17 | ) : BaseResource(name, "string-array") { 18 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 19 | super.serialize(ownerDocument, resourceCallback).apply { 20 | setAttribute("name", name) 21 | 22 | items.forEach { item -> 23 | appendChild(ownerDocument.createElement("item").also { itemNode -> 24 | itemNode.textContent = item 25 | }) 26 | } 27 | } 28 | 29 | companion object { 30 | fun fromNode(node: Node): ArrayResource { 31 | val key = node.attributes.getNamedItem("name").textContent 32 | val items = node.childElementsSequence().map { it.textContent }.toList() 33 | 34 | return ArrayResource(key, items) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/util/resource/BaseResource.kt: -------------------------------------------------------------------------------- 1 | package li.auna.util.resource 2 | 3 | import org.w3c.dom.Document 4 | import org.w3c.dom.Element 5 | 6 | /** 7 | * Base resource class for all resources. 8 | * 9 | * @param name The name of the resource. 10 | * @param tag The tag of the resource. 11 | */ 12 | @Suppress("MemberVisibilityCanBePrivate") 13 | abstract class BaseResource( 14 | val name: String, 15 | val tag: String 16 | ) { 17 | /** 18 | * Serialize resource element to XML. 19 | * Overriding methods should invoke super and operate on its return value. 20 | * @param ownerDocument Target document to create elements from. 21 | * @param resourceCallback Called when a resource has been processed. 22 | */ 23 | open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit = { }): Element { 24 | return ownerDocument.createElement(tag).apply { 25 | setAttribute("name", name) 26 | } 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var result = name.hashCode() 31 | result = 31 * result + tag.hashCode() 32 | return result 33 | } 34 | 35 | override fun equals(other: Any?): Boolean { 36 | if (this === other) return true 37 | if (javaClass != other?.javaClass) return false 38 | 39 | other as BaseResource 40 | 41 | return name == other.name 42 | } 43 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/youtube/layout/largepausebutton/LargePauseButtonPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.youtube.layout.largepausebutton 2 | 3 | import app.revanced.patcher.patch.resourcePatch 4 | 5 | @Suppress("unused") 6 | val largePauseButtonPatch = resourcePatch( 7 | name = "Large pause button", 8 | description = "Adds a large pause button to the player", 9 | ) { 10 | compatibleWith( 11 | "com.google.android.youtube", 12 | "app.revanced.android.youtube" 13 | ) 14 | 15 | execute { 16 | document("res/layout/youtube_controls_button_group_layout.xml").use { document -> 17 | val btnTouchArea = document.getElementsByTagName("FrameLayout").item(0) 18 | btnTouchArea.attributes.getNamedItem("android:layout_width").nodeValue = "180dp" 19 | btnTouchArea.attributes.getNamedItem("android:layout_height").nodeValue = "180dp" 20 | 21 | // modify the width and height of player_control_play_pause_replay_button element 22 | val btn = document.getElementsByTagName("com.google.android.libraries.youtube.common.ui.TouchImageView").item(0) 23 | btn.attributes.getNamedItem("android:layout_width").nodeValue = "112dp" 24 | btn.attributes.getNamedItem("android:layout_height").nodeValue = "112dp" 25 | // modify the padding of player_control_play_pause_replay_button element. default is 8dp 26 | btn.attributes.getNamedItem("android:padding").nodeValue = "16dp" 27 | } 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /extensions/instagram/src/main/java/app/revanced/extension/ShareLinkExtension.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.instagram; 2 | 3 | import app.revanced.extension.shared.Logger; 4 | import android.net.Uri; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class ShareLinkExtension { 9 | 10 | private static List trackerList; 11 | static { 12 | // Set of known tracker parameters. 13 | trackerList = Arrays.asList("igsh", "fbclid", "utm_source"); 14 | } 15 | 16 | /* 17 | * This method used to remove unnecessary trackers from url. 18 | * The method takes in an url as string and returns a clean url as string. 19 | */ 20 | public static String sanitizeUrl(String url){ 21 | try{ 22 | // Parse the URL. 23 | Uri uri = Uri.parse(url); 24 | 25 | // Build a clean url to append necessary parameters later. 26 | Uri.Builder uriBuilder = uri.buildUpon().clearQuery(); 27 | 28 | // Iterate over the existing query parameters and re-add them except the one to remove. 29 | for (String key : uri.getQueryParameterNames()) { 30 | if (!trackerList.contains(key)) { 31 | uriBuilder.appendQueryParameter(key, uri.getQueryParameter(key)); 32 | } 33 | } 34 | return uriBuilder.build().toString(); 35 | 36 | }catch (Exception ex){ 37 | Logger.printException(() -> "Instagram error", ex); 38 | } 39 | return url; 40 | } 41 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/interaction/bio/SelectableBioPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.interaction.bio 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 4 | import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 5 | import app.revanced.patcher.patch.bytecodePatch 6 | import com.android.tools.smali.dexlib2.Opcode 7 | import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction 8 | 9 | import li.auna.util.indexOfFirstInstructionOrThrow 10 | 11 | @Suppress("unused") 12 | val selectableBioPatch = bytecodePatch( 13 | name = "Selectable Bio", 14 | description = "Makes user's bio selectable.", 15 | ) { 16 | compatibleWith("com.instagram.android") 17 | 18 | execute { 19 | selectableBioFingerprint.method.apply { 20 | val setBioTextIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_VIRTUAL } 21 | val setTextViewInstruction = getInstruction(setBioTextIndex) 22 | val textViewRegister = setTextViewInstruction.registerC 23 | val textRegister = setTextViewInstruction.registerD 24 | 25 | // Make the textview selectable. 26 | addInstructions( 27 | setBioTextIndex + 1, 28 | """ 29 | const/4 v$textRegister, 0x1 30 | invoke-virtual { v$textViewRegister, v$textRegister }, Landroid/widget/TextView;->setTextIsSelectable(Z)V 31 | """, 32 | ) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - dev 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | permissions: 14 | contents: write 15 | packages: write 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | # Make sure the release step uses its own credentials: 22 | # https://github.com/cycjimmy/semantic-release-action#private-packages 23 | persist-credentials: false 24 | fetch-depth: 0 25 | 26 | - name: Setup Java 27 | uses: actions/setup-java@v4 28 | with: 29 | distribution: "temurin" 30 | java-version: "17" 31 | 32 | - name: Cache Gradle 33 | uses: burrunan/gradle-cache-action@v2 34 | 35 | - name: Build 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | run: ./gradlew build clean 39 | 40 | - name: Setup Node.js 41 | uses: actions/setup-node@v4 42 | with: 43 | node-version: "lts/*" 44 | cache: 'npm' 45 | 46 | - name: Install dependencies 47 | run: npm install 48 | 49 | - name: Import GPG key 50 | uses: crazy-max/ghaction-import-gpg@v6 51 | with: 52 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 53 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 54 | fingerprint: ${{ vars.GPG_FINGERPRINT }} 55 | 56 | - name: Release 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | run: npm exec semantic-release 60 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/util/resource/StringResource.kt: -------------------------------------------------------------------------------- 1 | package li.auna.util.resource 2 | 3 | import app.revanced.patcher.patch.PatchException 4 | import org.w3c.dom.Document 5 | import org.w3c.dom.Node 6 | 7 | /** 8 | * A string value. 9 | * Represents a string in the strings.xml file. 10 | * 11 | * @param name The name of the string. 12 | * @param value The value of the string. 13 | * @param formatted If the string is formatted. Defaults to `true`. 14 | */ 15 | class StringResource( 16 | name: String, 17 | val value: String, 18 | val formatted: Boolean = true, 19 | ) : BaseResource(name, "string") { 20 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 21 | super.serialize(ownerDocument, resourceCallback).apply { 22 | // if the string is un-formatted, explicitly add the formatted attribute 23 | if (!formatted) setAttribute("formatted", "false") 24 | 25 | if (value.contains(Regex("(?(index).registerA 26 | replaceInstruction( 27 | index, 28 | "const/4 v$instructionRegister, 0x1", 29 | ) 30 | } 31 | } 32 | } 33 | 34 | bypassIntegrityFingerprint.patch() 35 | spoofSignatureFingerprint.method.apply { 36 | addInstructions( 37 | 0, 38 | """ 39 | const-string v0, "49C1522548EBACD46CE322B6FD47F6092BB745D0F88082145CAF35E14DCC38E1" 40 | return-object v0 41 | """ 42 | ) 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👋🧩 ReVanced Experiments 2 | 3 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/Aunali321/RevancedExperiments/release.yml) 4 | ![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg) 5 | 6 | Collection of ReVanced Patches 7 | 8 | ## ❓ About 9 | 10 | Patches are small modifications to Android apps that allow you to change the behavior of or add new features, 11 | block ads, customize the appearance, and much more. 12 | 13 | ## 💪 Features 14 | 15 | Some of the features the patches provide are: 16 | 17 | * 🚫 **Block ads**: Say goodbye to ads 18 | * ⭐ **Customize your app**: Personalize the appearance of apps with various layouts and themes 19 | * 🪄 **Add new features**: Extend the functionality of apps with lots of new features 20 | * ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions, 21 | export activities, etc. 22 | * ✨ **And much more!** 23 | 24 | ## 🚀 How to get started 25 | 26 | You can use [ReVanced CLI](https://github.com/ReVanced/revanced-cli) or [ReVanced Manager](https://github.com/ReVanced/revanced-manager) to use ReVanced Experiments. 27 | 28 | 29 | ### 📙 Contributing 30 | 31 | Thank you for considering contributing to ReVanced Experiments. 32 | You can find the contribution guidelines [here](CONTRIBUTING.md). 33 | 34 | ### 🛠️ Building 35 | 36 | To build ReVanced Experiments, 37 | you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation). 38 | 39 | ## 📜 Licence 40 | 41 | ReVanced Experiments is licensed under the GPLv3 licence. 42 | Please see the [license file](LICENSE) for more information. 43 | [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute 44 | and modify ReVanced Patches template as long as you track changes/dates in source files. 45 | Any modifications to ReVanced Patches template must also be made available under the GPL, 46 | along with build & install instructions. 47 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/IntentPreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | 6 | /** 7 | * A preference that opens an intent. 8 | * 9 | * @param key Optional preference key. 10 | * @param titleKey The preference title key. 11 | * @param summaryKey The preference summary key. 12 | * @param tag The preference tag. 13 | * @param intent The intent to open. 14 | */ 15 | class IntentPreference( 16 | key: String? = null, 17 | titleKey: String = "${key}_title", 18 | summaryKey: String? = "${key}_summary", 19 | tag: String = "Preference", 20 | val intent: Intent, 21 | ) : BasePreference(key, titleKey, summaryKey, tag) { 22 | 23 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 24 | super.serialize(ownerDocument, resourceCallback).apply { 25 | appendChild(ownerDocument.createElement("intent").also { intentNode -> 26 | intentNode.setAttribute("android:data", intent.data) 27 | intentNode.setAttribute("android:targetClass", intent.targetClass) 28 | intentNode.setAttribute("android:targetPackage", intent.targetPackageSupplier()) 29 | }) 30 | } 31 | 32 | override fun equals(other: Any?): Boolean { 33 | if (this === other) return true 34 | if (javaClass != other?.javaClass) return false 35 | if (!super.equals(other)) return false 36 | 37 | other as IntentPreference 38 | 39 | return intent == other.intent 40 | } 41 | 42 | override fun hashCode(): Int { 43 | var result = super.hashCode() 44 | result = 31 * result + intent.hashCode() 45 | return result 46 | } 47 | 48 | data class Intent( 49 | internal val data: String, 50 | internal val targetClass: String, 51 | internal val targetPackageSupplier: () -> String, 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.requests; 2 | 3 | public class Route { 4 | private final String route; 5 | private final Method method; 6 | private final int paramCount; 7 | 8 | public Route(Method method, String route) { 9 | this.method = method; 10 | this.route = route; 11 | this.paramCount = countMatches(route, '{'); 12 | 13 | if (paramCount != countMatches(route, '}')) 14 | throw new IllegalArgumentException("Not enough parameters"); 15 | } 16 | 17 | public Method getMethod() { 18 | return method; 19 | } 20 | 21 | public CompiledRoute compile(String... params) { 22 | if (params.length != paramCount) 23 | throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " + 24 | "Expected: " + paramCount + ", provided: " + params.length); 25 | 26 | StringBuilder compiledRoute = new StringBuilder(route); 27 | for (int i = 0; i < paramCount; i++) { 28 | int paramStart = compiledRoute.indexOf("{"); 29 | int paramEnd = compiledRoute.indexOf("}"); 30 | compiledRoute.replace(paramStart, paramEnd + 1, params[i]); 31 | } 32 | return new CompiledRoute(this, compiledRoute.toString()); 33 | } 34 | 35 | public static class CompiledRoute { 36 | private final Route baseRoute; 37 | private final String compiledRoute; 38 | 39 | private CompiledRoute(Route baseRoute, String compiledRoute) { 40 | this.baseRoute = baseRoute; 41 | this.compiledRoute = compiledRoute; 42 | } 43 | 44 | public String getCompiledRoute() { 45 | return compiledRoute; 46 | } 47 | 48 | public Method getMethod() { 49 | return baseRoute.method; 50 | } 51 | } 52 | 53 | private int countMatches(CharSequence seq, char c) { 54 | int count = 0; 55 | for (int i = 0; i < seq.length(); i++) { 56 | if (seq.charAt(i) == c) 57 | count++; 58 | } 59 | return count; 60 | } 61 | 62 | public enum Method { 63 | GET, 64 | POST 65 | } 66 | } -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import java.util.Locale; 4 | 5 | public enum AppLanguage { 6 | /** 7 | * The current app language. 8 | */ 9 | DEFAULT, 10 | 11 | // Language codes found in locale_config.xml 12 | // All region specific variants have been removed. 13 | AF, 14 | AM, 15 | AR, 16 | AS, 17 | AZ, 18 | BE, 19 | BG, 20 | BN, 21 | BS, 22 | CA, 23 | CS, 24 | DA, 25 | DE, 26 | EL, 27 | EN, 28 | ES, 29 | ET, 30 | EU, 31 | FA, 32 | FI, 33 | FR, 34 | GL, 35 | GU, 36 | HI, 37 | HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code. 38 | HR, 39 | HU, 40 | HY, 41 | ID, 42 | IS, 43 | IT, 44 | JA, 45 | KA, 46 | KK, 47 | KM, 48 | KN, 49 | KO, 50 | KY, 51 | LO, 52 | LT, 53 | LV, 54 | MK, 55 | ML, 56 | MN, 57 | MR, 58 | MS, 59 | MY, 60 | NE, 61 | NL, 62 | NB, 63 | OR, 64 | PA, 65 | PL, 66 | PT, 67 | RO, 68 | RU, 69 | SI, 70 | SK, 71 | SL, 72 | SQ, 73 | SR, 74 | SV, 75 | SW, 76 | TA, 77 | TE, 78 | TH, 79 | TL, 80 | TR, 81 | UK, 82 | UR, 83 | UZ, 84 | VI, 85 | ZH, 86 | ZU; 87 | 88 | private final String language; 89 | 90 | AppLanguage() { 91 | language = name().toLowerCase(Locale.US); 92 | } 93 | 94 | /** 95 | * @return The 2 letter ISO 639_1 language code. 96 | */ 97 | public String getLanguage() { 98 | // Changing the app language does not force the app to completely restart, 99 | // so the default needs to be the current language and not a static field. 100 | if (this == DEFAULT) { 101 | return Locale.getDefault().getLanguage(); 102 | } 103 | 104 | return language; 105 | } 106 | 107 | public Locale getLocale() { 108 | if (this == DEFAULT) { 109 | return Locale.getDefault(); 110 | } 111 | 112 | return Locale.forLanguageTag(language); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/all/misc/packagename/ChangePackageNamePatch.kt: -------------------------------------------------------------------------------- 1 | package app.revanced.patches.all.misc.packagename 2 | 3 | import app.revanced.patcher.patch.Option 4 | import app.revanced.patcher.patch.resourcePatch 5 | import app.revanced.patcher.patch.stringOption 6 | import org.w3c.dom.Element 7 | 8 | lateinit var packageNameOption: Option 9 | 10 | /** 11 | * Set the package name to use. 12 | * If this is called multiple times, the first call will set the package name. 13 | * 14 | * @param fallbackPackageName The package name to use if the user has not already specified a package name. 15 | * @return The package name that was set. 16 | * @throws OptionException.ValueValidationException If the package name is invalid. 17 | */ 18 | fun setOrGetFallbackPackageName(fallbackPackageName: String): String { 19 | val packageName = packageNameOption.value!! 20 | 21 | return if (packageName == packageNameOption.default) { 22 | fallbackPackageName.also { packageNameOption.value = it } 23 | } else { 24 | packageName 25 | } 26 | } 27 | 28 | val changePackageNamePatch = resourcePatch( 29 | name = "Change package name", 30 | description = "Appends \".revanced\" to the package name by default. Changing the package name of the app can lead to unexpected issues.", 31 | use = false, 32 | ) { 33 | packageNameOption = stringOption( 34 | key = "packageName", 35 | default = "Default", 36 | values = mapOf("Default" to "Default"), 37 | title = "Package name", 38 | description = "The name of the package to rename the app to.", 39 | required = true, 40 | ) { 41 | it == "Default" || it!!.matches(Regex("^[a-z]\\w*(\\.[a-z]\\w*)+\$")) 42 | } 43 | 44 | finalize { 45 | document("AndroidManifest.xml").use { document -> 46 | 47 | val replacementPackageName = packageNameOption.value 48 | 49 | val manifest = document.getElementsByTagName("manifest").item(0) as Element 50 | manifest.setAttribute( 51 | "package", 52 | if (replacementPackageName != packageNameOption.default) { 53 | replacementPackageName 54 | } else { 55 | "${manifest.getAttribute("package")}.revanced" 56 | }, 57 | ) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/BasePreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | import org.w3c.dom.Element 6 | 7 | /** 8 | * Base preference class for all preferences. 9 | * 10 | * @param key The key of the preference. If null, other parameters must be specified. 11 | * @param titleKey The key of the preference title. 12 | * @param summaryKey The key of the preference summary. 13 | * @param tag The tag or full class name of the preference. 14 | */ 15 | @Suppress("MemberVisibilityCanBePrivate") 16 | abstract class BasePreference( 17 | val key: String? = null, 18 | val titleKey: String = "${key}_title", 19 | val summaryKey: String? = "${key}_summary", 20 | val tag: String 21 | ) { 22 | /** 23 | * Serialize preference element to XML. 24 | * Overriding methods should invoke super and operate on its return value. 25 | * 26 | * @param resourceCallback A callback for additional resources. 27 | * @param ownerDocument Target document to create elements from. 28 | * 29 | * @return The serialized element. 30 | */ 31 | open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit): Element = 32 | ownerDocument.createElement(tag).apply { 33 | key?.let { setAttribute("android:key", it) } 34 | setAttribute("android:title", "@string/${titleKey}") 35 | summaryKey?.let { addSummary(it) } 36 | } 37 | 38 | override fun hashCode(): Int { 39 | var result = key?.hashCode() ?: 0 40 | result = 31 * result + titleKey.hashCode() 41 | result = 31 * result + tag.hashCode() 42 | return result 43 | } 44 | 45 | override fun equals(other: Any?): Boolean { 46 | if (this === other) return true 47 | if (javaClass != other?.javaClass) return false 48 | 49 | other as BasePreference 50 | 51 | if (key != other.key) return false 52 | if (titleKey != other.titleKey) return false 53 | if (tag != other.tag) return false 54 | 55 | return true 56 | } 57 | 58 | companion object { 59 | fun Element.addSummary(summaryKey: String, summaryType: SummaryType = SummaryType.DEFAULT) = 60 | setAttribute("android:${summaryType.type}", "@string/$summaryKey") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/PreferenceScreenPreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.BaseResource 4 | import org.w3c.dom.Document 5 | 6 | /** 7 | * A preference screen. 8 | * 9 | * @param key The key of the preference. If null, other parameters must be specified. 10 | * @param titleKey The key of the preference title. 11 | * @param summaryKey The key of the preference summary. 12 | * @param sorting Sorting to use. If the sorting is not [Sorting.UNSORTED], 13 | * then the key parameter will be modified to include the sort type. 14 | * @param tag The tag or full class name of the preference. 15 | * @param preferences The preferences in this screen. 16 | */ 17 | @Suppress("MemberVisibilityCanBePrivate") 18 | open class PreferenceScreenPreference( 19 | key: String? = null, 20 | titleKey: String = "${key}_title", 21 | summaryKey: String? = "${key}_summary", 22 | sorting: Sorting = Sorting.BY_TITLE, 23 | tag: String = "PreferenceScreen", 24 | val preferences: Set, 25 | // Alternatively, instead of repurposing the key for sorting, 26 | // an extra bundle parameter can be added to the preferences XML declaration. 27 | // This would require bundling and referencing an additional XML file 28 | // or adding new attributes to the attrs.xml file. 29 | // Since the key value is not currently used by the extensions, 30 | // for now it's much simpler to modify the key to include the sort parameter. 31 | ) : BasePreference(if (sorting == Sorting.UNSORTED) key else (key + sorting.keySuffix), titleKey, summaryKey, tag) { 32 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 33 | super.serialize(ownerDocument, resourceCallback).apply { 34 | preferences.forEach { 35 | appendChild(it.serialize(ownerDocument, resourceCallback)) 36 | } 37 | } 38 | 39 | /** 40 | * How a PreferenceScreen should be sorted. 41 | */ 42 | enum class Sorting(val keySuffix: String) { 43 | /** 44 | * Sort by the localized preference title. 45 | */ 46 | BY_TITLE("_sort_by_title"), 47 | 48 | /** 49 | * Sort by the preference keys. 50 | */ 51 | BY_KEY("_sort_by_key"), 52 | 53 | /** 54 | * Unspecified sorting. 55 | */ 56 | UNSORTED("_sort_by_unsorted"), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/SettingsPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings 2 | 3 | import app.revanced.patcher.patch.resourcePatch 4 | import li.auna.patches.all.misc.resources.addResource 5 | import li.auna.patches.all.misc.resources.addResources 6 | import li.auna.patches.all.misc.resources.addResourcesPatch 7 | import li.auna.patches.shared.misc.settings.preference.BasePreference 8 | import li.auna.patches.shared.misc.settings.preference.IntentPreference 9 | import li.auna.util.ResourceGroup 10 | import li.auna.util.copyResources 11 | import li.auna.util.getNode 12 | import li.auna.util.insertFirst 13 | import org.w3c.dom.Node 14 | 15 | /** 16 | * A resource patch that adds settings to a settings fragment. 17 | * 18 | * @param rootPreference A pair of an intent preference and the name of the fragment file to add it to. 19 | * If null, no preference will be added. 20 | * @param preferences A set of preferences to add to the ReVanced fragment. 21 | */ 22 | fun settingsPatch( 23 | rootPreference: Pair? = null, 24 | preferences: Set, 25 | ) = resourcePatch { 26 | dependsOn(addResourcesPatch) 27 | 28 | execute { 29 | copyResources( 30 | "settings", 31 | ResourceGroup("xml", "revanced_prefs.xml"), 32 | ) 33 | 34 | addResources("shared", "misc.settings.settingsResourcePatch") 35 | } 36 | 37 | finalize { 38 | fun Node.addPreference(preference: BasePreference, prepend: Boolean = false) { 39 | preference.serialize(ownerDocument) { resource -> 40 | // TODO: Currently, resources can only be added to "values", which may not be the correct place. 41 | // It may be necessary to ask for the desired resourceValue in the future. 42 | addResource("values", resource) 43 | }.let { preferenceNode -> 44 | insertFirst(preferenceNode) 45 | } 46 | } 47 | 48 | // Add the root preference to an existing fragment if needed. 49 | rootPreference?.let { (intentPreference, fragment) -> 50 | document("res/xml/$fragment.xml").use { document -> 51 | document.getNode("PreferenceScreen").addPreference(intentPreference, true) 52 | } 53 | } 54 | 55 | // Add all preferences to the ReVanced fragment. 56 | document("res/xml/revanced_prefs.xml").use { document -> 57 | val revancedPreferenceScreenNode = document.getNode("PreferenceScreen") 58 | preferences.forEach { revancedPreferenceScreenNode.addPreference(it) } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import static java.lang.Boolean.FALSE; 4 | import static java.lang.Boolean.TRUE; 5 | import static app.revanced.extension.shared.settings.Setting.parent; 6 | import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability; 7 | import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.SpoofiOSAvailability; 8 | 9 | import app.revanced.extension.shared.spoof.ClientType; 10 | 11 | /** 12 | * Settings shared across multiple apps. 13 | *

14 | * To ensure this class is loaded when the UI is created, app specific setting bundles should extend 15 | * or reference this class. 16 | */ 17 | public class BaseSettings { 18 | public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE); 19 | public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG)); 20 | public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message"); 21 | 22 | public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); 23 | 24 | public static final EnumSetting REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true, "revanced_language_user_dialog_message"); 25 | 26 | public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message"); 27 | public static final EnumSetting SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability()); 28 | public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS)); 29 | public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true, 30 | "revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofiOSAvailability()); 31 | // Client type must be last spoof setting due to cyclic references. 32 | public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS)); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /assets/revanced-logo/revanced-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings.preference; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.preference.EditTextPreference; 7 | import android.util.AttributeSet; 8 | import android.widget.Button; 9 | import android.widget.EditText; 10 | 11 | import app.revanced.extension.shared.Utils; 12 | import app.revanced.extension.shared.settings.Setting; 13 | import app.revanced.extension.shared.Logger; 14 | 15 | import java.util.Objects; 16 | 17 | import static app.revanced.extension.shared.StringRef.str; 18 | 19 | @SuppressWarnings({"unused", "deprecation"}) 20 | public class ResettableEditTextPreference extends EditTextPreference { 21 | 22 | public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 23 | super(context, attrs, defStyleAttr, defStyleRes); 24 | } 25 | public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { 26 | super(context, attrs, defStyleAttr); 27 | } 28 | public ResettableEditTextPreference(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | } 31 | public ResettableEditTextPreference(Context context) { 32 | super(context); 33 | } 34 | 35 | @Override 36 | protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { 37 | super.onPrepareDialogBuilder(builder); 38 | Utils.setEditTextDialogTheme(builder); 39 | 40 | Setting setting = Setting.getSettingFromPath(getKey()); 41 | if (setting != null) { 42 | builder.setNeutralButton(str("revanced_settings_reset"), null); 43 | } 44 | } 45 | 46 | @Override 47 | protected void showDialog(Bundle state) { 48 | super.showDialog(state); 49 | 50 | // Override the button click listener to prevent dismissing the dialog. 51 | Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL); 52 | if (button == null) { 53 | return; 54 | } 55 | button.setOnClickListener(v -> { 56 | try { 57 | Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); 58 | String defaultStringValue = setting.defaultValue.toString(); 59 | EditText editText = getEditText(); 60 | editText.setText(defaultStringValue); 61 | editText.setSelection(defaultStringValue.length()); // move cursor to end of text 62 | } catch (Exception ex) { 63 | Logger.printException(() -> "reset failure", ex); 64 | } 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/ListPreference.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.util.resource.ArrayResource 4 | import li.auna.util.resource.BaseResource 5 | import org.w3c.dom.Document 6 | 7 | /** 8 | * List preference. 9 | * 10 | * @param key The preference key. If null, other parameters must be specified. 11 | * @param titleKey The preference title key. 12 | * @param summaryKey The preference summary key. 13 | * @param tag The preference tag. 14 | * @param entriesKey The entries array key. 15 | * @param entryValuesKey The entry values array key. 16 | */ 17 | @Suppress("MemberVisibilityCanBePrivate") 18 | class ListPreference( 19 | key: String? = null, 20 | titleKey: String = "${key}_title", 21 | summaryKey: String? = "${key}_summary", 22 | tag: String = "ListPreference", 23 | val entriesKey: String? = "${key}_entries", 24 | val entryValuesKey: String? = "${key}_entry_values" 25 | ) : BasePreference(key, titleKey, summaryKey, tag) { 26 | var entries: ArrayResource? = null 27 | private set 28 | var entryValues: ArrayResource? = null 29 | private set 30 | 31 | /** 32 | * List preference. 33 | * 34 | * @param key The preference key. If null, other parameters must be specified. 35 | * @param titleKey The preference title key. 36 | * @param summaryKey The preference summary key. 37 | * @param tag The preference tag. 38 | * @param entries The entries array. 39 | * @param entryValues The entry values array. 40 | */ 41 | constructor( 42 | key: String? = null, 43 | titleKey: String = "${key}_title", 44 | summaryKey: String? = "${key}_summary", 45 | tag: String = "ListPreference", 46 | entries: ArrayResource, 47 | entryValues: ArrayResource 48 | ) : this(key, titleKey, summaryKey, tag, entries.name, entryValues.name) { 49 | this.entries = entries 50 | this.entryValues = entryValues 51 | } 52 | 53 | override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = 54 | super.serialize(ownerDocument, resourceCallback).apply { 55 | val entriesArrayName = entries?.also { resourceCallback.invoke(it) }?.name ?: entriesKey 56 | val entryValuesArrayName = entryValues?.also { resourceCallback.invoke(it) }?.name ?: entryValuesKey 57 | 58 | entriesArrayName?.let { 59 | setAttribute( 60 | "android:entries", 61 | "@array/$it" 62 | ) 63 | } 64 | 65 | entryValuesArrayName?.let { 66 | setAttribute( 67 | "android:entryValues", 68 | "@array/$it" 69 | ) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/mapping/ResourceMappingPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.mapping 2 | 3 | import app.revanced.patcher.patch.PatchException 4 | import app.revanced.patcher.patch.resourcePatch 5 | import org.w3c.dom.Element 6 | import java.util.* 7 | import java.util.concurrent.Executors 8 | import java.util.concurrent.TimeUnit 9 | 10 | // TODO: Probably renaming the patch/this is a good idea. 11 | lateinit var resourceMappings: List 12 | private set 13 | 14 | val resourceMappingPatch = resourcePatch { 15 | val threadCount = Runtime.getRuntime().availableProcessors() 16 | val threadPoolExecutor = Executors.newFixedThreadPool(threadCount) 17 | 18 | val resourceMappings = Collections.synchronizedList(mutableListOf()) 19 | 20 | execute { 21 | // Save the file in memory to concurrently read from it. 22 | val resourceXmlFile = get("res/values/public.xml").readBytes() 23 | 24 | for (threadIndex in 0 until threadCount) { 25 | threadPoolExecutor.execute thread@{ 26 | document(resourceXmlFile.inputStream()).use { document -> 27 | 28 | val resources = document.documentElement.childNodes 29 | val resourcesLength = resources.length 30 | val jobSize = resourcesLength / threadCount 31 | 32 | val batchStart = jobSize * threadIndex 33 | val batchEnd = jobSize * (threadIndex + 1) 34 | element@ for (i in batchStart until batchEnd) { 35 | // Prevent out of bounds. 36 | if (i >= resourcesLength) return@thread 37 | 38 | val node = resources.item(i) 39 | if (node !is Element) continue 40 | 41 | val nameAttribute = node.getAttribute("name") 42 | val typeAttribute = node.getAttribute("type") 43 | 44 | if (node.nodeName != "public" || nameAttribute.startsWith("APKTOOL")) continue 45 | 46 | val id = node.getAttribute("id").substring(2).toLong(16) 47 | 48 | resourceMappings.add(ResourceElement(typeAttribute, nameAttribute, id)) 49 | } 50 | } 51 | } 52 | } 53 | 54 | threadPoolExecutor.also { it.shutdown() }.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS) 55 | 56 | li.auna.patches.shared.misc.mapping.resourceMappings = resourceMappings 57 | } 58 | } 59 | 60 | operator fun List.get(type: String, name: String) = resourceMappings.firstOrNull { 61 | it.type == type && it.name == name 62 | }?.id ?: throw PatchException("Could not find resource type: $type name: $name") 63 | 64 | data class ResourceElement internal constructor(val type: String, val name: String, val id: Long) 65 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class LongSetting extends Setting { 13 | 14 | public LongSetting(String key, Long defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public LongSetting(String key, Long defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public LongSetting(String key, Long defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public LongSetting(String key, Long defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getLongString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return json.getLong(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Long.valueOf(Objects.requireNonNull(newValue)); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull Long newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveLongString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Long get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class StringSetting extends Setting { 13 | 14 | public StringSetting(String key, String defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public StringSetting(String key, String defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public StringSetting(String key, String defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public StringSetting(String key, String defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return json.getString(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Objects.requireNonNull(newValue); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull String newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public String get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class FloatSetting extends Setting { 13 | 14 | public FloatSetting(String key, Float defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public FloatSetting(String key, Float defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public FloatSetting(String key, Float defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public FloatSetting(String key, Float defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getFloatString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return (float) json.getDouble(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Float.valueOf(Objects.requireNonNull(newValue)); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull Float newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveFloatString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Float get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class IntegerSetting extends Setting { 13 | 14 | public IntegerSetting(String key, Integer defaultValue) { 15 | super(key, defaultValue); 16 | } 17 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) { 18 | super(key, defaultValue, rebootApp); 19 | } 20 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) { 21 | super(key, defaultValue, rebootApp, includeWithImportExport); 22 | } 23 | public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) { 24 | super(key, defaultValue, userDialogMessage); 25 | } 26 | public IntegerSetting(String key, Integer defaultValue, Availability availability) { 27 | super(key, defaultValue, availability); 28 | } 29 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) { 30 | super(key, defaultValue, rebootApp, userDialogMessage); 31 | } 32 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) { 33 | super(key, defaultValue, rebootApp, availability); 34 | } 35 | public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 36 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 37 | } 38 | public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 39 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 40 | } 41 | 42 | @Override 43 | protected void load() { 44 | value = preferences.getIntegerString(key, defaultValue); 45 | } 46 | 47 | @Override 48 | protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException { 49 | return json.getInt(importExportKey); 50 | } 51 | 52 | @Override 53 | protected void setValueFromString(@NonNull String newValue) { 54 | value = Integer.valueOf(Objects.requireNonNull(newValue)); 55 | } 56 | 57 | @Override 58 | public void save(@NonNull Integer newValue) { 59 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 60 | value = Objects.requireNonNull(newValue); 61 | preferences.saveIntegerString(key, newValue); 62 | } 63 | 64 | @NonNull 65 | @Override 66 | public Integer get() { 67 | return value; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | # Compiled class file 3 | *.class 4 | 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | ### JetBrains template 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 29 | 30 | # User-specific stuff 31 | .idea/**/workspace.xml 32 | .idea/**/tasks.xml 33 | .idea/**/usage.statistics.xml 34 | .idea/**/dictionaries 35 | .idea/**/shelf 36 | 37 | # Generated files 38 | .idea/**/contentModel.xml 39 | 40 | # Sensitive or high-churn files 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.local.xml 44 | .idea/**/sqlDataSources.xml 45 | .idea/**/dynamic.xml 46 | .idea/**/uiDesigner.xml 47 | .idea/**/dbnavigator.xml 48 | 49 | # Gradle 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # Gradle and Maven with auto-import 54 | # When using Gradle or Maven with auto-import, you should exclude module files, 55 | # since they will be recreated, and may cause churn. Uncomment if using 56 | # auto-import. 57 | .idea/artifacts 58 | .idea/compiler.xml 59 | .idea/jarRepositories.xml 60 | .idea/modules.xml 61 | .idea/*.iml 62 | .idea/modules 63 | *.iml 64 | *.ipr 65 | 66 | # CMake 67 | cmake-build-*/ 68 | 69 | # Mongo Explorer plugin 70 | .idea/**/mongoSettings.xml 71 | 72 | # File-based project format 73 | *.iws 74 | 75 | # IntelliJ 76 | out/ 77 | 78 | # mpeltonen/sbt-idea plugin 79 | .idea_modules/ 80 | 81 | # JIRA plugin 82 | atlassian-ide-plugin.xml 83 | 84 | # Cursive Clojure plugin 85 | .idea/replstate.xml 86 | 87 | # Crashlytics plugin (for Android Studio and IntelliJ) 88 | com_crashlytics_export_strings.xml 89 | crashlytics.properties 90 | crashlytics-build.properties 91 | fabric.properties 92 | 93 | # Editor-based Rest Client 94 | .idea/httpRequests 95 | 96 | # Android studio 3.1+ serialized cache file 97 | .idea/caches/build_file_checksums.ser 98 | 99 | ### Gradle template 100 | .gradle 101 | **/build/ 102 | !src/**/build/ 103 | 104 | # Ignore Gradle GUI config 105 | gradle-app.setting 106 | 107 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 108 | !gradle-wrapper.jar 109 | 110 | # Cache of project 111 | .gradletasknamecache 112 | 113 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 114 | # gradle/wrapper/gradle-wrapper.properties 115 | 116 | # Potentially copyrighted test APK 117 | *.apk 118 | 119 | # Ignore vscode config 120 | .vscode/ 121 | 122 | # Dependency directories 123 | node_modules/ 124 | 125 | # gradle properties, due to Github token 126 | ./gradle.properties 127 | 128 | # Ignore IDEA files 129 | .idea/ 130 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/settings/preference/BasePreferenceScreen.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.settings.preference 2 | 3 | import li.auna.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting 4 | import java.io.Closeable 5 | 6 | abstract class BasePreferenceScreen( 7 | private val root: MutableSet = mutableSetOf(), 8 | ) : Closeable { 9 | 10 | override fun close() { 11 | if (root.isEmpty()) return 12 | 13 | root.forEach { preference -> 14 | commit(preference.transform()) 15 | } 16 | } 17 | 18 | /** 19 | * Finalize and insert root preference into resource patch 20 | */ 21 | abstract fun commit(screen: PreferenceScreenPreference) 22 | 23 | open inner class Screen( 24 | key: String? = null, 25 | titleKey: String = "${key}_title", 26 | private val summaryKey: String? = "${key}_summary", 27 | preferences: MutableSet = mutableSetOf(), 28 | val categories: MutableSet = mutableSetOf(), 29 | private val sorting: Sorting = Sorting.BY_TITLE, 30 | ) : BasePreferenceCollection(key, titleKey, preferences) { 31 | 32 | override fun transform(): PreferenceScreenPreference { 33 | return PreferenceScreenPreference( 34 | key, 35 | titleKey, 36 | summaryKey, 37 | sorting, 38 | // Screens and preferences are sorted at runtime by extension code, 39 | // so title sorting uses the localized language in use. 40 | preferences = preferences + categories.map { it.transform() }, 41 | ) 42 | } 43 | 44 | private fun ensureScreenInserted() { 45 | // Add to screens if not yet done 46 | if (!root.contains(this)) { 47 | root.add(this) 48 | } 49 | } 50 | 51 | fun addPreferences(vararg preferences: BasePreference) { 52 | ensureScreenInserted() 53 | this.preferences.addAll(preferences) 54 | } 55 | 56 | open inner class Category( 57 | key: String? = null, 58 | titleKey: String = "${key}_title", 59 | preferences: MutableSet = mutableSetOf(), 60 | ) : BasePreferenceCollection(key, titleKey, preferences) { 61 | override fun transform(): PreferenceCategory { 62 | return PreferenceCategory( 63 | key, 64 | titleKey, 65 | preferences = preferences, 66 | ) 67 | } 68 | 69 | fun addPreferences(vararg preferences: BasePreference) { 70 | ensureScreenInserted() 71 | 72 | // Add to the categories if not done yet. 73 | if (!categories.contains(this)) { 74 | categories.add(this) 75 | } 76 | 77 | this.preferences.addAll(preferences) 78 | } 79 | } 80 | } 81 | 82 | abstract class BasePreferenceCollection( 83 | val key: String? = null, 84 | val titleKey: String = "${key}_title", 85 | val preferences: MutableSet = mutableSetOf(), 86 | ) { 87 | abstract fun transform(): BasePreference 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Objects; 10 | 11 | @SuppressWarnings("unused") 12 | public class BooleanSetting extends Setting { 13 | public BooleanSetting(String key, Boolean defaultValue) { 14 | super(key, defaultValue); 15 | } 16 | public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) { 17 | super(key, defaultValue, rebootApp); 18 | } 19 | public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) { 20 | super(key, defaultValue, rebootApp, includeWithImportExport); 21 | } 22 | public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) { 23 | super(key, defaultValue, userDialogMessage); 24 | } 25 | public BooleanSetting(String key, Boolean defaultValue, Availability availability) { 26 | super(key, defaultValue, availability); 27 | } 28 | public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) { 29 | super(key, defaultValue, rebootApp, userDialogMessage); 30 | } 31 | public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) { 32 | super(key, defaultValue, rebootApp, availability); 33 | } 34 | public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 35 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 36 | } 37 | public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 38 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 39 | } 40 | 41 | /** 42 | * Sets, but does _not_ persistently save the value. 43 | * This method is only to be used by the Settings preference code. 44 | * 45 | * This intentionally is a static method to deter 46 | * accidental usage when {@link #save(Boolean)} was intnded. 47 | */ 48 | public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) { 49 | setting.value = Objects.requireNonNull(newValue); 50 | } 51 | 52 | @Override 53 | protected void load() { 54 | value = preferences.getBoolean(key, defaultValue); 55 | } 56 | 57 | @Override 58 | protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException { 59 | return json.getBoolean(importExportKey); 60 | } 61 | 62 | @Override 63 | protected void setValueFromString(@NonNull String newValue) { 64 | value = Boolean.valueOf(Objects.requireNonNull(newValue)); 65 | } 66 | 67 | @Override 68 | public void save(@NonNull Boolean newValue) { 69 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 70 | value = Objects.requireNonNull(newValue); 71 | preferences.saveBoolean(key, newValue); 72 | } 73 | 74 | @NonNull 75 | @Override 76 | public Boolean get() { 77 | return value; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.spoof.requests; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.io.IOException; 7 | import java.net.HttpURLConnection; 8 | 9 | import app.revanced.extension.shared.Logger; 10 | import app.revanced.extension.shared.requests.Requester; 11 | import app.revanced.extension.shared.requests.Route; 12 | import app.revanced.extension.shared.settings.BaseSettings; 13 | import app.revanced.extension.shared.settings.AppLanguage; 14 | import app.revanced.extension.shared.spoof.ClientType; 15 | 16 | final class PlayerRoutes { 17 | static final Route.CompiledRoute GET_STREAMING_DATA = new Route( 18 | Route.Method.POST, 19 | "player" + 20 | "?fields=streamingData" + 21 | "&alt=proto" 22 | ).compile(); 23 | 24 | private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; 25 | 26 | /** 27 | * TCP connection and HTTP read timeout 28 | */ 29 | private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. 30 | 31 | private PlayerRoutes() { 32 | } 33 | 34 | static String createInnertubeBody(ClientType clientType) { 35 | JSONObject innerTubeBody = new JSONObject(); 36 | 37 | try { 38 | JSONObject context = new JSONObject(); 39 | 40 | // Can override default language only if no login is used. 41 | // Could use preferred audio for all clients that do not login, 42 | // but if this is a fall over client it will set the language even though 43 | // the audio language is not selectable in the UI. 44 | ClientType userSelectedClient = BaseSettings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); 45 | AppLanguage language = userSelectedClient == ClientType.ANDROID_VR_NO_AUTH 46 | ? BaseSettings.SPOOF_VIDEO_STREAMS_LANGUAGE.get() 47 | : AppLanguage.DEFAULT; 48 | 49 | JSONObject client = new JSONObject(); 50 | client.put("hl", language.getLanguage()); 51 | client.put("clientName", clientType.clientName); 52 | client.put("clientVersion", clientType.clientVersion); 53 | client.put("deviceModel", clientType.deviceModel); 54 | client.put("osVersion", clientType.osVersion); 55 | if (clientType.androidSdkVersion != null) { 56 | client.put("androidSdkVersion", clientType.androidSdkVersion); 57 | } 58 | context.put("client", client); 59 | 60 | innerTubeBody.put("context", context); 61 | innerTubeBody.put("contentCheckOk", true); 62 | innerTubeBody.put("racyCheckOk", true); 63 | innerTubeBody.put("videoId", "%s"); 64 | } catch (JSONException e) { 65 | Logger.printException(() -> "Failed to create innerTubeBody", e); 66 | } 67 | 68 | return innerTubeBody.toString(); 69 | } 70 | 71 | /** 72 | * @noinspection SameParameterValue 73 | */ 74 | static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { 75 | var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); 76 | 77 | connection.setRequestProperty("Content-Type", "application/json"); 78 | connection.setRequestProperty("User-Agent", clientType.userAgent); 79 | 80 | connection.setUseCaches(false); 81 | connection.setDoOutput(true); 82 | 83 | connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); 84 | connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); 85 | return connection; 86 | } 87 | } -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings.preference; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.preference.EditTextPreference; 7 | import android.preference.Preference; 8 | import android.text.InputType; 9 | import android.util.AttributeSet; 10 | import android.util.TypedValue; 11 | import android.widget.EditText; 12 | import app.revanced.extension.shared.settings.Setting; 13 | import app.revanced.extension.shared.Logger; 14 | import app.revanced.extension.shared.Utils; 15 | 16 | import static app.revanced.extension.shared.StringRef.str; 17 | 18 | @SuppressWarnings({"unused", "deprecation"}) 19 | public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { 20 | 21 | private String existingSettings; 22 | 23 | private void init() { 24 | setSelectable(true); 25 | 26 | EditText editText = getEditText(); 27 | editText.setTextIsSelectable(true); 28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 29 | editText.setAutofillHints((String) null); 30 | } 31 | editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 32 | editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap. 33 | 34 | setOnPreferenceClickListener(this); 35 | } 36 | 37 | public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 38 | super(context, attrs, defStyleAttr, defStyleRes); 39 | init(); 40 | } 41 | public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) { 42 | super(context, attrs, defStyleAttr); 43 | init(); 44 | } 45 | public ImportExportPreference(Context context, AttributeSet attrs) { 46 | super(context, attrs); 47 | init(); 48 | } 49 | public ImportExportPreference(Context context) { 50 | super(context); 51 | init(); 52 | } 53 | 54 | @Override 55 | public boolean onPreferenceClick(Preference preference) { 56 | try { 57 | // Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. 58 | existingSettings = Setting.exportToJson(getContext()); 59 | getEditText().setText(existingSettings); 60 | } catch (Exception ex) { 61 | Logger.printException(() -> "showDialog failure", ex); 62 | } 63 | return true; 64 | } 65 | 66 | @Override 67 | protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { 68 | try { 69 | Utils.setEditTextDialogTheme(builder); 70 | 71 | // Show the user the settings in JSON format. 72 | builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { 73 | Utils.setClipboard(getEditText().getText().toString()); 74 | }).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> { 75 | importSettings(builder.getContext(), getEditText().getText().toString()); 76 | }); 77 | } catch (Exception ex) { 78 | Logger.printException(() -> "onPrepareDialogBuilder failure", ex); 79 | } 80 | } 81 | 82 | private void importSettings(Context context, String replacementSettings) { 83 | try { 84 | if (replacementSettings.equals(existingSettings)) { 85 | return; 86 | } 87 | AbstractPreferenceFragment.settingImportInProgress = true; 88 | 89 | final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); 90 | if (rebootNeeded) { 91 | AbstractPreferenceFragment.showRestartDialog(getContext()); 92 | } 93 | } catch (Exception ex) { 94 | Logger.printException(() -> "importSettings failure", ex); 95 | } finally { 96 | AbstractPreferenceFragment.settingImportInProgress = false; 97 | } 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class StringRef { 13 | private static Resources resources; 14 | private static String packageName; 15 | 16 | // must use a thread safe map, as this class is used both on and off the main thread 17 | private static final Map strings = Collections.synchronizedMap(new HashMap<>()); 18 | 19 | /** 20 | * Returns a cached instance. 21 | * Should be used if the same String could be loaded more than once. 22 | * 23 | * @param id string resource name/id 24 | * @see #sf(String) 25 | */ 26 | @NonNull 27 | public static StringRef sfc(@NonNull String id) { 28 | StringRef ref = strings.get(id); 29 | if (ref == null) { 30 | ref = new StringRef(id); 31 | strings.put(id, ref); 32 | } 33 | return ref; 34 | } 35 | 36 | /** 37 | * Creates a new instance, but does not cache the value. 38 | * Should be used for Strings that are loaded exactly once. 39 | * 40 | * @param id string resource name/id 41 | * @see #sfc(String) 42 | */ 43 | @NonNull 44 | public static StringRef sf(@NonNull String id) { 45 | return new StringRef(id); 46 | } 47 | 48 | /** 49 | * Gets string value by string id, shorthand for sfc(id).toString() 50 | * 51 | * @param id string resource name/id 52 | * @return String value from string.xml 53 | */ 54 | @NonNull 55 | public static String str(@NonNull String id) { 56 | return sfc(id).toString(); 57 | } 58 | 59 | /** 60 | * Gets string value by string id, shorthand for sfc(id).toString() and formats the string 61 | * with given args. 62 | * 63 | * @param id string resource name/id 64 | * @param args the args to format the string with 65 | * @return String value from string.xml formatted with given args 66 | */ 67 | @NonNull 68 | public static String str(@NonNull String id, Object... args) { 69 | return String.format(str(id), args); 70 | } 71 | 72 | /** 73 | * Creates a StringRef object that'll not change it's value 74 | * 75 | * @param value value which toString() method returns when invoked on returned object 76 | * @return Unique StringRef instance, its value will never change 77 | */ 78 | @NonNull 79 | public static StringRef constant(@NonNull String value) { 80 | final StringRef ref = new StringRef(value); 81 | ref.resolved = true; 82 | return ref; 83 | } 84 | 85 | /** 86 | * Shorthand for constant("") 87 | * Its value always resolves to empty string 88 | */ 89 | @NonNull 90 | public static final StringRef empty = constant(""); 91 | 92 | @NonNull 93 | private String value; 94 | private boolean resolved; 95 | 96 | public StringRef(@NonNull String resName) { 97 | this.value = resName; 98 | } 99 | 100 | @Override 101 | @NonNull 102 | public String toString() { 103 | if (!resolved) { 104 | if (resources == null || packageName == null) { 105 | Context context = Utils.getContext(); 106 | resources = context.getResources(); 107 | packageName = context.getPackageName(); 108 | } 109 | resolved = true; 110 | if (resources != null) { 111 | final int identifier = resources.getIdentifier(value, "string", packageName); 112 | if (identifier == 0) 113 | Logger.printException(() -> "Resource not found: " + value); 114 | else 115 | value = resources.getString(identifier); 116 | } else { 117 | Logger.printException(() -> "Could not resolve resources!"); 118 | } 119 | } 120 | return value; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/instagram/misc/quality/MaxMediaQualityPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.instagram.misc.quality 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstruction 4 | import app.revanced.patcher.extensions.InstructionExtensions.getInstruction 5 | import app.revanced.patcher.extensions.InstructionExtensions.instructions 6 | import app.revanced.patcher.patch.bytecodePatch 7 | import com.android.tools.smali.dexlib2.Opcode 8 | import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction 9 | import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction 10 | 11 | @Suppress("unused") 12 | val maxMediaQualityPatch = bytecodePatch( 13 | name = "Max Media Quality", 14 | description = "Enable max media quality.", 15 | ) { 16 | compatibleWith( 17 | "com.instagram.android" 18 | ) 19 | 20 | execute { 21 | val maxPostSize = "2048" // Maximum post size. 22 | val maxBitRate = "10000000" // Maximum bit rate possible (found in code). 23 | 24 | // Improve quality of images. 25 | // Instagram tend to reduce/compress the image resolution to user's device height and width. 26 | // This section of code removes that restriction and sets the resolution to 2048x2048 (max possible). 27 | displayMetricsFingerprint.let { it -> 28 | it.method.apply { 29 | val displayMetInstructions = instructions.filter { it.opcode == Opcode.IGET } 30 | 31 | // There are 3 iget instances. 32 | // 1.dpi 2.width 3.height. 33 | // We don't need to change dpi, we just need to change height and width. 34 | displayMetInstructions.drop(1).forEach { instruction -> 35 | val index = instruction.location.index 36 | val register = getInstruction(index).registerA 37 | 38 | // Set height and width to 2048. 39 | addInstruction(index + 1, "const v$register, $maxPostSize") 40 | } 41 | } 42 | } 43 | 44 | // Yet another method where the image resolution is compressed. 45 | mediaSizeFingerprint.let { it -> 46 | it.classDef.apply { 47 | val mediaSetMethod = 48 | methods.first { it.returnType == "Lcom/instagram/model/mediasize/ExtendedImageUrl;" } 49 | 50 | val mediaSetInstructions = 51 | mediaSetMethod.instructions.filter { it.opcode == Opcode.INVOKE_VIRTUAL } 52 | 53 | mediaSetInstructions.forEach { instruction -> 54 | val index = instruction.location.index + 1 55 | val register = mediaSetMethod.getInstruction(index).registerA 56 | 57 | // Set height and width to 2048. 58 | mediaSetMethod.addInstruction(index + 1, "const v$register, $maxPostSize") 59 | } 60 | } 61 | } 62 | 63 | // Improve quality of stories. 64 | // This section of code sets the bitrate of the stories to the maximum possible. 65 | storyMediaBitrateFingerprint.let { it -> 66 | it.method.apply { 67 | val ifLezIndex = instructions.first { it.opcode == Opcode.IF_LEZ }.location.index 68 | 69 | val bitRateRegister = getInstruction(ifLezIndex).registerA 70 | 71 | // Set the bitrate to maximum possible. 72 | addInstruction(ifLezIndex + 1, "const v$bitRateRegister, $maxBitRate") 73 | } 74 | } 75 | 76 | // Improve quality of reels. 77 | // In general Instagram tend to set the minimum bitrate between maximum possible and compressed video's bitrate. 78 | // This section of code sets the bitrate of the reels to the maximum possible. 79 | videoEncoderConfigFingerprint.let { it -> 80 | it.classDef.apply { 81 | // Get the constructor. 82 | val videoEncoderConfigConstructor = methods.first() 83 | 84 | val lastMoveResIndex = videoEncoderConfigConstructor.instructions 85 | .last { it.opcode == Opcode.MOVE_RESULT }.location.index 86 | 87 | // Finding the register were the bitrate is stored. 88 | val bitRateRegister = 89 | videoEncoderConfigConstructor.getInstruction(lastMoveResIndex).registerA 90 | 91 | // Set bitrate to maximum possible. 92 | videoEncoderConfigConstructor.addInstruction( 93 | lastMoveResIndex + 1, 94 | "const v$bitRateRegister, $maxBitRate", 95 | ) 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/checks/BaseCheckEnvironmentPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.checks 2 | 3 | import android.os.Build.* 4 | import app.revanced.patcher.Fingerprint 5 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 6 | import app.revanced.patcher.patch.Patch 7 | import app.revanced.patcher.patch.bytecodePatch 8 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue 9 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableLongEncodedValue 10 | import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableStringEncodedValue 11 | import li.auna.patches.all.misc.resources.addResources 12 | import li.auna.patches.all.misc.resources.addResourcesPatch 13 | import com.android.tools.smali.dexlib2.immutable.value.ImmutableLongEncodedValue 14 | import com.android.tools.smali.dexlib2.immutable.value.ImmutableStringEncodedValue 15 | import java.nio.charset.StandardCharsets 16 | import java.security.MessageDigest 17 | import kotlin.io.encoding.Base64 18 | import kotlin.io.encoding.ExperimentalEncodingApi 19 | 20 | private const val EXTENSION_CLASS_DESCRIPTOR = 21 | "Lapp/revanced/extension/shared/checks/CheckEnvironmentPatch;" 22 | 23 | fun checkEnvironmentPatch( 24 | mainActivityOnCreateFingerprint: Fingerprint, 25 | extensionPatch: Patch<*>, 26 | vararg compatiblePackages: String, 27 | ) = bytecodePatch( 28 | description = "Checks, if the application was patched by, otherwise warns the user.", 29 | ) { 30 | compatibleWith(*compatiblePackages) 31 | 32 | dependsOn( 33 | extensionPatch, 34 | addResourcesPatch, 35 | ) 36 | 37 | execute { 38 | addResources("shared", "misc.checks.checkEnvironmentPatch") 39 | 40 | fun setPatchInfo() { 41 | fun Fingerprint.setClassFields(vararg fieldNameValues: Pair) { 42 | val fieldNameValueMap = mapOf(*fieldNameValues) 43 | 44 | classDef.fields.forEach { field -> 45 | field.initialValue = fieldNameValueMap[field.name] ?: return@forEach 46 | } 47 | } 48 | 49 | patchInfoFingerprint.setClassFields( 50 | "PATCH_TIME" to System.currentTimeMillis().encoded, 51 | ) 52 | 53 | fun setBuildInfo() { 54 | patchInfoBuildFingerprint.setClassFields( 55 | "PATCH_BOARD" to BOARD.encodedAndHashed, 56 | "PATCH_BOOTLOADER" to BOOTLOADER.encodedAndHashed, 57 | "PATCH_BRAND" to BRAND.encodedAndHashed, 58 | "PATCH_CPU_ABI" to CPU_ABI.encodedAndHashed, 59 | "PATCH_CPU_ABI2" to CPU_ABI2.encodedAndHashed, 60 | "PATCH_DEVICE" to DEVICE.encodedAndHashed, 61 | "PATCH_DISPLAY" to DISPLAY.encodedAndHashed, 62 | "PATCH_FINGERPRINT" to FINGERPRINT.encodedAndHashed, 63 | "PATCH_HARDWARE" to HARDWARE.encodedAndHashed, 64 | "PATCH_HOST" to HOST.encodedAndHashed, 65 | "PATCH_ID" to ID.encodedAndHashed, 66 | "PATCH_MANUFACTURER" to MANUFACTURER.encodedAndHashed, 67 | "PATCH_MODEL" to MODEL.encodedAndHashed, 68 | "PATCH_PRODUCT" to PRODUCT.encodedAndHashed, 69 | "PATCH_RADIO" to RADIO.encodedAndHashed, 70 | "PATCH_TAGS" to TAGS.encodedAndHashed, 71 | "PATCH_TYPE" to TYPE.encodedAndHashed, 72 | "PATCH_USER" to USER.encodedAndHashed, 73 | ) 74 | } 75 | 76 | try { 77 | Class.forName("android.os.Build") 78 | // This only works on Android, 79 | // because it uses Android APIs. 80 | setBuildInfo() 81 | } catch (_: ClassNotFoundException) { 82 | } 83 | } 84 | 85 | fun invokeCheck() = mainActivityOnCreateFingerprint.method.addInstructions( 86 | 0, 87 | "invoke-static/range { p0 .. p0 },$EXTENSION_CLASS_DESCRIPTOR->check(Landroid/app/Activity;)V", 88 | ) 89 | 90 | setPatchInfo() 91 | invokeCheck() 92 | } 93 | } 94 | 95 | @OptIn(ExperimentalEncodingApi::class) 96 | private val String.encodedAndHashed 97 | get() = MutableStringEncodedValue( 98 | ImmutableStringEncodedValue( 99 | Base64.encode( 100 | MessageDigest.getInstance("SHA-1") 101 | .digest(this.toByteArray(StandardCharsets.UTF_8)), 102 | ), 103 | ), 104 | ) 105 | 106 | private val Long.encoded get() = MutableLongEncodedValue(ImmutableLongEncodedValue(this)) 107 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.spoof; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.Nullable; 6 | 7 | import app.revanced.extension.shared.settings.BaseSettings; 8 | 9 | public enum ClientType { 10 | // https://dumps.tadiphone.dev/dumps/oculus/eureka 11 | ANDROID_VR_NO_AUTH( 12 | 28, 13 | "ANDROID_VR", 14 | "Quest 3", 15 | "12", 16 | "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip", 17 | "32", // Android 12.1 18 | "1.56.21", 19 | false, 20 | "Android VR No auth" 21 | ), 22 | ANDROID_UNPLUGGED( 23 | 29, 24 | "ANDROID_UNPLUGGED", 25 | "Google TV Streamer", 26 | "14", 27 | "com.google.android.apps.youtube.unplugged/8.49.0 (Linux; U; Android 14; GB) gzip", 28 | "34", 29 | "8.49.0", 30 | true, 31 | "Android TV" 32 | ), 33 | ANDROID_VR( 34 | ANDROID_VR_NO_AUTH.id, 35 | ANDROID_VR_NO_AUTH.clientName, 36 | ANDROID_VR_NO_AUTH.deviceModel, 37 | ANDROID_VR_NO_AUTH.osVersion, 38 | ANDROID_VR_NO_AUTH.userAgent, 39 | ANDROID_VR_NO_AUTH.androidSdkVersion, 40 | ANDROID_VR_NO_AUTH.clientVersion, 41 | true, 42 | "Android VR" 43 | ), 44 | IOS_UNPLUGGED(33, 45 | "IOS_UNPLUGGED", 46 | forceAVC() 47 | ? "iPhone12,5" // 11 Pro Max (last device with iOS 13) 48 | : "iPhone16,2", // 15 Pro Max 49 | // iOS 13 and earlier uses only AVC. 14+ adds VP9 and AV1. 50 | forceAVC() 51 | ? "13.7.17H35" // Last release of iOS 13. 52 | : "18.1.1.22B91", 53 | forceAVC() 54 | ? "com.google.ios.youtubeunplugged/6.45 (iPhone; U; CPU iOS 13_7 like Mac OS X)" 55 | : "com.google.ios.youtubeunplugged/8.33 (iPhone; U; CPU iOS 18_1_1 like Mac OS X)", 56 | null, 57 | // Version number should be a valid iOS release. 58 | // https://www.ipa4fun.com/history/152043/ 59 | // Some newer versions can also force AVC, 60 | // but 6.45 is the last version that supports iOS 13. 61 | forceAVC() 62 | ? "6.45" 63 | : "8.33", 64 | true, 65 | forceAVC() 66 | ? "iOS TV Force AVC" 67 | : "iOS TV" 68 | ); 69 | 70 | private static boolean forceAVC() { 71 | return BaseSettings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get(); 72 | } 73 | 74 | /** 75 | * YouTube 76 | * client type 77 | */ 78 | public final int id; 79 | 80 | public final String clientName; 81 | 82 | /** 83 | * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) 84 | */ 85 | public final String deviceModel; 86 | 87 | /** 88 | * Device OS version. 89 | */ 90 | public final String osVersion; 91 | 92 | /** 93 | * Player user-agent. 94 | */ 95 | public final String userAgent; 96 | 97 | /** 98 | * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) 99 | * Field is null if not applicable. 100 | */ 101 | @Nullable 102 | public final String androidSdkVersion; 103 | 104 | /** 105 | * App version. 106 | */ 107 | public final String clientVersion; 108 | 109 | /** 110 | * If the client can access the API logged in. 111 | */ 112 | public final boolean canLogin; 113 | 114 | /** 115 | * Friendly name displayed in stats for nerds. 116 | */ 117 | public final String friendlyName; 118 | 119 | ClientType(int id, 120 | String clientName, 121 | String deviceModel, 122 | String osVersion, 123 | String userAgent, 124 | @Nullable String androidSdkVersion, 125 | String clientVersion, 126 | boolean canLogin, 127 | String friendlyName) { 128 | this.id = id; 129 | this.clientName = clientName; 130 | this.deviceModel = deviceModel; 131 | this.osVersion = osVersion; 132 | this.userAgent = userAgent; 133 | this.androidSdkVersion = androidSdkVersion; 134 | this.clientVersion = clientVersion; 135 | this.canLogin = canLogin; 136 | this.friendlyName = friendlyName; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/extension/SharedExtensionPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.extension 2 | 3 | import app.revanced.patcher.Fingerprint 4 | import app.revanced.patcher.FingerprintBuilder 5 | import app.revanced.patcher.extensions.InstructionExtensions.addInstruction 6 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 7 | import app.revanced.patcher.fingerprint 8 | import app.revanced.patcher.patch.BytecodePatchContext 9 | import app.revanced.patcher.patch.PatchException 10 | import app.revanced.patcher.patch.bytecodePatch 11 | import com.android.tools.smali.dexlib2.iface.Method 12 | import java.net.URLDecoder 13 | import java.util.jar.JarFile 14 | 15 | internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/Utils;" 16 | 17 | /** 18 | * A patch to extend with an extension shared with multiple patches. 19 | * 20 | * @param extensionName The name of the extension to extend with. 21 | */ 22 | fun sharedExtensionPatch( 23 | extensionName: String, 24 | vararg hooks: ExtensionHook, 25 | ) = bytecodePatch { 26 | dependsOn(sharedExtensionPatch(*hooks)) 27 | 28 | extendWith("extensions/$extensionName.rve") 29 | } 30 | 31 | /** 32 | * A patch to extend with the "shared" extension. 33 | * 34 | * @param hooks The hooks to get the application context for use in the extension, 35 | * commonly for the onCreate method of exported activities. 36 | */ 37 | fun sharedExtensionPatch( 38 | vararg hooks: ExtensionHook, 39 | ) = bytecodePatch { 40 | extendWith("extensions/shared.rve") 41 | 42 | execute { 43 | if (classes.none { EXTENSION_CLASS_DESCRIPTOR == it.type }) { 44 | throw PatchException( 45 | "Shared extension has not been merged yet. This patch can not succeed without merging it.", 46 | ) 47 | } 48 | 49 | hooks.forEach { hook -> hook(EXTENSION_CLASS_DESCRIPTOR) } 50 | 51 | // Modify Utils method to include the patches release version. 52 | revancedUtilsPatchesVersionFingerprint.method.apply { 53 | /** 54 | * @return The file path for the jar this classfile is contained inside. 55 | */ 56 | fun getCurrentJarFilePath(): String { 57 | val className = object {}::class.java.enclosingClass.name.replace('.', '/') + ".class" 58 | val classUrl = object {}::class.java.classLoader?.getResource(className) 59 | if (classUrl != null) { 60 | val urlString = classUrl.toString() 61 | 62 | if (urlString.startsWith("jar:file:")) { 63 | val end = urlString.lastIndexOf('!') 64 | 65 | return URLDecoder.decode(urlString.substring("jar:file:".length, end), "UTF-8") 66 | } 67 | } 68 | throw IllegalStateException("Not running from inside a JAR file.") 69 | } 70 | 71 | /** 72 | * @return The value for the manifest entry, 73 | * or "Unknown" if the entry does not exist or is blank. 74 | */ 75 | @Suppress("SameParameterValue") 76 | fun getPatchesManifestEntry(attributeKey: String) = JarFile(getCurrentJarFilePath()).use { jarFile -> 77 | jarFile.manifest.mainAttributes.entries.firstOrNull { it.key.toString() == attributeKey }?.value?.toString() 78 | ?: "Unknown" 79 | } 80 | 81 | val manifestValue = getPatchesManifestEntry("Version") 82 | 83 | addInstructions( 84 | 0, 85 | """ 86 | const-string v0, "$manifestValue" 87 | return-object v0 88 | """, 89 | ) 90 | } 91 | } 92 | } 93 | 94 | class ExtensionHook internal constructor( 95 | private val fingerprint: Fingerprint, 96 | private val insertIndexResolver: ((Method) -> Int), 97 | private val contextRegisterResolver: (Method) -> String, 98 | ) { 99 | context(BytecodePatchContext) 100 | operator fun invoke(extensionClassDescriptor: String) { 101 | val insertIndex = insertIndexResolver(fingerprint.method) 102 | val contextRegister = contextRegisterResolver(fingerprint.method) 103 | 104 | fingerprint.method.addInstruction( 105 | insertIndex, 106 | "invoke-static/range { $contextRegister .. $contextRegister }, " + 107 | "$extensionClassDescriptor->setContext(Landroid/content/Context;)V", 108 | ) 109 | } 110 | } 111 | 112 | fun extensionHook( 113 | insertIndexResolver: ((Method) -> Int) = { 0 }, 114 | contextRegisterResolver: (Method) -> String = { "p0" }, 115 | fingerprintBuilderBlock: FingerprintBuilder.() -> Unit, 116 | ) = ExtensionHook(fingerprint(block = fingerprintBuilderBlock), insertIndexResolver, contextRegisterResolver) -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.Locale; 10 | import java.util.Objects; 11 | 12 | import app.revanced.extension.shared.Logger; 13 | 14 | /** 15 | * If an Enum value is removed or changed, any saved or imported data using the 16 | * non-existent value will be reverted to the default value 17 | * (the event is logged, but no user error is displayed). 18 | * 19 | * All saved JSON text is converted to lowercase to keep the output less obnoxious. 20 | */ 21 | @SuppressWarnings("unused") 22 | public class EnumSetting> extends Setting { 23 | public EnumSetting(String key, T defaultValue) { 24 | super(key, defaultValue); 25 | } 26 | public EnumSetting(String key, T defaultValue, boolean rebootApp) { 27 | super(key, defaultValue, rebootApp); 28 | } 29 | public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) { 30 | super(key, defaultValue, rebootApp, includeWithImportExport); 31 | } 32 | public EnumSetting(String key, T defaultValue, String userDialogMessage) { 33 | super(key, defaultValue, userDialogMessage); 34 | } 35 | public EnumSetting(String key, T defaultValue, Availability availability) { 36 | super(key, defaultValue, availability); 37 | } 38 | public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) { 39 | super(key, defaultValue, rebootApp, userDialogMessage); 40 | } 41 | public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) { 42 | super(key, defaultValue, rebootApp, availability); 43 | } 44 | public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { 45 | super(key, defaultValue, rebootApp, userDialogMessage, availability); 46 | } 47 | public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { 48 | super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); 49 | } 50 | 51 | @Override 52 | protected void load() { 53 | value = preferences.getEnum(key, defaultValue); 54 | } 55 | 56 | @Override 57 | protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException { 58 | String enumName = json.getString(importExportKey); 59 | try { 60 | return getEnumFromString(enumName); 61 | } catch (IllegalArgumentException ex) { 62 | // Info level to allow removing enum values in the future without showing any user errors. 63 | Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex); 64 | return defaultValue; 65 | } 66 | } 67 | 68 | @Override 69 | protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException { 70 | // Use lowercase to keep the output less ugly. 71 | json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH)); 72 | } 73 | 74 | @NonNull 75 | private T getEnumFromString(String enumName) { 76 | //noinspection ConstantConditions 77 | for (Enum value : defaultValue.getClass().getEnumConstants()) { 78 | if (value.name().equalsIgnoreCase(enumName)) { 79 | // noinspection unchecked 80 | return (T) value; 81 | } 82 | } 83 | throw new IllegalArgumentException("Unknown enum value: " + enumName); 84 | } 85 | 86 | @Override 87 | protected void setValueFromString(@NonNull String newValue) { 88 | value = getEnumFromString(Objects.requireNonNull(newValue)); 89 | } 90 | 91 | @Override 92 | public void save(@NonNull T newValue) { 93 | // Must set before saving to preferences (otherwise importing fails to update UI correctly). 94 | value = Objects.requireNonNull(newValue); 95 | preferences.saveEnumAsString(key, newValue); 96 | } 97 | 98 | @NonNull 99 | @Override 100 | public T get() { 101 | return value; 102 | } 103 | 104 | /** 105 | * Availability based on if this setting is currently set to any of the provided types. 106 | */ 107 | @SafeVarargs 108 | public final Setting.Availability availability(@NonNull T... types) { 109 | return () -> { 110 | T currentEnumType = get(); 111 | for (T enumType : types) { 112 | if (currentEnumType == enumType) return true; 113 | } 114 | return false; 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/telegram/downloadboost/DownloadBoostPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.telegram.downloadboost 2 | 3 | import app.revanced.patcher.extensions.InstructionExtensions.addInstructions 4 | import app.revanced.patcher.patch.bytecodePatch 5 | import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable 6 | import com.android.tools.smali.dexlib2.AccessFlags 7 | import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation 8 | import com.android.tools.smali.dexlib2.immutable.ImmutableMethod 9 | 10 | @Suppress("unused") 11 | val downloadBoostPatch = bytecodePatch( 12 | name = "Download Speed Boost", 13 | description = "Boosts download speed", 14 | ) { 15 | compatibleWith( 16 | "org.telegram.messenger", 17 | "org.telegram.messenger.web", 18 | "uz.unnarsx.cherrygram" 19 | ) 20 | 21 | execute { 22 | val className = updateParamsFingerprint.originalClassDef.type 23 | val originalMethod = updateParamsFingerprint.method 24 | val returnType = originalMethod.returnType 25 | 26 | updateParamsFingerprint.classDef.methods.removeIf { it.name == originalMethod.name } 27 | 28 | updateParamsFingerprint.classDef.methods.add( 29 | ImmutableMethod( 30 | className, 31 | originalMethod.name, 32 | emptyList(), 33 | returnType, 34 | AccessFlags.PRIVATE.value, 35 | null, 36 | null, 37 | MutableMethodImplementation(5) 38 | ).toMutable().apply { 39 | addInstructions( 40 | """ 41 | .line 266 42 | iget v0, p0, Lorg/telegram/messenger/FileLoadOperation;->preloadPrefixSize:I 43 | 44 | if-gtz v0, :cond_e 45 | 46 | iget v0, p0, Lorg/telegram/messenger/FileLoadOperation;->currentAccount:I 47 | 48 | invoke-static {v0}, Lorg/telegram/messenger/MessagesController;->getInstance(I)Lorg/telegram/messenger/MessagesController; 49 | 50 | move-result-object v0 51 | 52 | iget-boolean v0, v0, Lorg/telegram/messenger/MessagesController;->getfileExperimentalParams:Z 53 | 54 | if-eqz v0, :cond_1d 55 | 56 | :cond_e 57 | iget-boolean v0, p0, Lorg/telegram/messenger/FileLoadOperation;->forceSmallChunk:Z 58 | 59 | if-nez v0, :cond_1d 60 | 61 | const/high16 v0, 0x80000 62 | 63 | .line 267 64 | iput v0, p0, Lorg/telegram/messenger/FileLoadOperation;->downloadChunkSizeBig:I 65 | 66 | const/16 v0, 0x8 67 | 68 | .line 268 69 | iput v0, p0, Lorg/telegram/messenger/FileLoadOperation;->maxDownloadRequests:I 70 | 71 | .line 269 72 | iput v0, p0, Lorg/telegram/messenger/FileLoadOperation;->maxDownloadRequestsBig:I 73 | 74 | goto :goto_26 75 | 76 | :cond_1d 77 | const/high16 v0, 0x80000 78 | 79 | .line 271 80 | iput v0, p0, Lorg/telegram/messenger/FileLoadOperation;->downloadChunkSizeBig:I 81 | 82 | const/16 v0, 0x8 83 | 84 | .line 272 85 | iput v0, p0, Lorg/telegram/messenger/FileLoadOperation;->maxDownloadRequests:I 86 | 87 | .line 273 88 | iput v0, p0, Lorg/telegram/messenger/FileLoadOperation;->maxDownloadRequestsBig:I 89 | 90 | goto :goto_26 91 | 92 | :goto_26 93 | const-wide/32 v0, 0x7d000000 94 | 95 | .line 275 96 | iget v2, p0, Lorg/telegram/messenger/FileLoadOperation;->downloadChunkSizeBig:I 97 | 98 | int-to-long v2, v2 99 | 100 | div-long/2addr v0, v2 101 | 102 | long-to-int v1, v0 103 | 104 | iput v1, p0, Lorg/telegram/messenger/FileLoadOperation;->maxCdnParts:I 105 | 106 | return-void 107 | """ 108 | ) 109 | } 110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/patches/shared/misc/hex/HexPatch.kt: -------------------------------------------------------------------------------- 1 | package li.auna.patches.shared.misc.hex 2 | 3 | import app.revanced.patcher.patch.PatchException 4 | import app.revanced.patcher.patch.rawResourcePatch 5 | import kotlin.math.max 6 | 7 | // The replacements being passed using a function is intended. 8 | // Previously the replacements were a property of the patch. Getter were being delegated to that property. 9 | // This late evaluation was being leveraged in app.revanced.patches.all.misc.hex.HexPatch. 10 | // Without the function, the replacements would be evaluated at the time of patch creation. 11 | // This isn't possible because the delegated property is not accessible at that time. 12 | fun hexPatch(replacementsSupplier: () -> Set) = rawResourcePatch { 13 | execute { 14 | replacementsSupplier().groupBy { it.targetFilePath }.forEach { (targetFilePath, replacements) -> 15 | val targetFile = try { 16 | get(targetFilePath, true) 17 | } catch (e: Exception) { 18 | throw PatchException("Could not find target file: $targetFilePath") 19 | } 20 | 21 | // TODO: Use a file channel to read and write the file instead of reading the whole file into memory, 22 | // in order to reduce memory usage. 23 | val targetFileBytes = targetFile.readBytes() 24 | 25 | replacements.forEach { replacement -> 26 | replacement.replacePattern(targetFileBytes) 27 | } 28 | 29 | targetFile.writeBytes(targetFileBytes) 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * Represents a pattern to search for and its replacement pattern. 36 | * 37 | * @property pattern The pattern to search for. 38 | * @property replacementPattern The pattern to replace the [pattern] with. 39 | * @property targetFilePath The path to the file to make the changes in relative to the APK root. 40 | */ 41 | class Replacement( 42 | private val pattern: String, 43 | replacementPattern: String, 44 | internal val targetFilePath: String, 45 | ) { 46 | private val patternBytes = pattern.toByteArrayPattern() 47 | private val replacementPattern = replacementPattern.toByteArrayPattern() 48 | 49 | init { 50 | if (this.patternBytes.size != this.replacementPattern.size) { 51 | throw PatchException("Pattern and replacement pattern must have the same length: $pattern") 52 | } 53 | } 54 | 55 | /** 56 | * Replaces the [patternBytes] with the [replacementPattern] in the [targetFileBytes]. 57 | * 58 | * @param targetFileBytes The bytes of the file to make the changes in. 59 | */ 60 | fun replacePattern(targetFileBytes: ByteArray) { 61 | val startIndex = indexOfPatternIn(targetFileBytes) 62 | 63 | if (startIndex == -1) { 64 | throw PatchException("Pattern not found in target file: $pattern") 65 | } 66 | 67 | replacementPattern.copyInto(targetFileBytes, startIndex) 68 | } 69 | 70 | // TODO: Allow searching in a file channel instead of a byte array to reduce memory usage. 71 | /** 72 | * Returns the index of the first occurrence of [patternBytes] in the haystack 73 | * using the Boyer-Moore algorithm. 74 | * 75 | * @param haystack The array to search in. 76 | * 77 | * @return The index of the first occurrence of the [patternBytes] in the haystack or -1 78 | * if the [patternBytes] is not found. 79 | */ 80 | private fun indexOfPatternIn(haystack: ByteArray): Int { 81 | val needle = patternBytes 82 | 83 | val haystackLength = haystack.size - 1 84 | val needleLength = needle.size - 1 85 | val right = IntArray(256) { -1 } 86 | 87 | for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i 88 | 89 | var skip: Int 90 | for (i in 0..haystackLength - needleLength) { 91 | skip = 0 92 | 93 | for (j in needleLength - 1 downTo 0) { 94 | if (needle[j] != haystack[i + j]) { 95 | skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)]) 96 | 97 | break 98 | } 99 | } 100 | 101 | if (skip == 0) return i 102 | } 103 | return -1 104 | } 105 | 106 | companion object { 107 | /** 108 | * Convert a string representing a pattern of hexadecimal bytes to a byte array. 109 | * 110 | * @return The byte array representing the pattern. 111 | * @throws PatchException If the pattern is invalid. 112 | */ 113 | private fun String.toByteArrayPattern() = try { 114 | split(" ").map { it.toInt(16).toByte() }.toByteArray() 115 | } catch (e: NumberFormatException) { 116 | throw PatchException( 117 | "Could not parse pattern: $this. A pattern is a sequence of case insensitive strings " + 118 | "representing hexadecimal bytes separated by spaces", 119 | e, 120 | ) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 8 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 |     20 | 21 | 22 | 23 | 24 | 25 |     26 | 27 | 28 | 29 | 30 | 31 |     32 | 33 | 34 | 35 | 36 | 37 |     38 | 39 | 40 | 41 | 42 | 43 |     44 | 45 | 46 | 47 | 48 | 49 |     50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 | Continuing the legacy of Vanced 59 |

60 | 61 | # 👋 Contribution guidelines 62 | 63 | This document describes how to contribute to ReVanced Patches template. 64 | 65 | ## 📖 Resources to help you get started 66 | 67 | * [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on 68 | * [Issues](https://github.com/ReVanced/revanced-patches-template/issues) are where we keep track of bugs and feature requests 69 | 70 | ## 🙏 Submitting a feature request 71 | 72 | Features can be requested by opening an issue using the 73 | [Feature request issue template](https://github.com/ReVanced/revanced-patches-template/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+). 74 | 75 | > **Note** 76 | > Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patches template. 77 | > Good motivation has to be provided for a request to be accepted. 78 | 79 | ## 🐞 Submitting a bug report 80 | 81 | If you encounter a bug while using ReVanced Patches template, open an issue using the 82 | [Bug report issue template](https://github.com/ReVanced/revanced-patches-template/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+). 83 | 84 | ## 📝 How to contribute 85 | 86 | 1. Before contributing, it is recommended to open an issue to discuss your change 87 | with the maintainers of ReVanced Patches template. This will help you determine whether your change is acceptable 88 | and whether it is worth your time to implement it 89 | 2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev` 90 | 3. Commit your changes 91 | 4. Submit a pull request to the `dev` branch of the repository and reference issues 92 | that your pull request closes in the description of your pull request 93 | 5. Our team will review your pull request and provide feedback. Once your pull request is approved, 94 | it will be merged into the `dev` branch and will be included in the next release of ReVanced Patches template 95 | 96 | ❤️ Thank you for considering contributing to ReVanced Patches template, 97 | ReVanced 98 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ⭐ Feature request 2 | description: Create a detailed request for a new feature. 3 | title: "feat: " 4 | labels: ["Feature request"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 |

10 | 11 | 16 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 |     28 | 29 | 30 | 31 | 32 | 33 |     34 | 35 | 36 | 37 | 38 | 39 |     40 | 41 | 42 | 43 | 44 | 45 |     46 | 47 | 48 | 49 | 50 | 51 |     52 | 53 | 54 | 55 | 56 | 57 |     58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 |
66 | Continuing the legacy of Vanced 67 |

68 | 69 | # ReVanced Patches template feature request 70 | 71 | Before creating a new feature request, please keep the following in mind: 72 | 73 | - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches-template/issues?q=label%3A%22Feature+request%22). 74 | - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches-template/blob/main/CONTRIBUTING.md). 75 | - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). 76 | - type: textarea 77 | attributes: 78 | label: Feature description 79 | description: | 80 | - Describe your feature in detail 81 | - Add images, videos, links, examples, references, etc. if possible 82 | - type: textarea 83 | attributes: 84 | label: Motivation 85 | description: | 86 | A strong motivation is necessary for a feature request to be considered. 87 | 88 | - Why should this feature be implemented? 89 | - What is the explicit use case? 90 | - What are the benefits? 91 | - What makes this feature important? 92 | validations: 93 | required: true 94 | - type: checkboxes 95 | id: acknowledgements 96 | attributes: 97 | label: Acknowledgements 98 | description: Your feature request will be closed if you don't follow the checklist below. 99 | options: 100 | - label: I have checked all open and closed feature requests and this is not a duplicate 101 | required: true 102 | - label: I have chosen an appropriate title. 103 | required: true 104 | - label: All requested information has been provided properly. 105 | required: true 106 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report a bug or an issue. 3 | title: "bug: " 4 | labels: ["Bug report"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 |

10 | 11 | 16 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 |     28 | 29 | 30 | 31 | 32 | 33 |     34 | 35 | 36 | 37 | 38 | 39 |     40 | 41 | 42 | 43 | 44 | 45 |     46 | 47 | 48 | 49 | 50 | 51 |     52 | 53 | 54 | 55 | 56 | 57 |     58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 |
66 | Continuing the legacy of Vanced 67 |

68 | 69 | # ReVanced Patches template bug report 70 | 71 | Before creating a new bug report, please keep the following in mind: 72 | 73 | - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches-template/issues?q=label%3A%22Bug+report%22). 74 | - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches-template/blob/main/CONTRIBUTING.md). 75 | - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). 76 | - type: textarea 77 | attributes: 78 | label: Bug description 79 | description: | 80 | - Describe your bug in detail 81 | - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) 82 | - Add images and videos if possible 83 | validations: 84 | required: true 85 | - type: textarea 86 | attributes: 87 | label: Error logs 88 | description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell. 89 | render: shell 90 | - type: textarea 91 | attributes: 92 | label: Solution 93 | description: If applicable, add a possible solution to the bug. 94 | - type: textarea 95 | attributes: 96 | label: Additional context 97 | description: Add additional context here. 98 | - type: checkboxes 99 | id: acknowledgements 100 | attributes: 101 | label: Acknowledgements 102 | description: Your bug report will be closed if you don't follow the checklist below. 103 | options: 104 | - label: I have checked all open and closed bug reports and this is not a duplicate. 105 | required: true 106 | - label: I have chosen an appropriate title. 107 | required: true 108 | - label: All requested information has been provided properly. 109 | required: true 110 | -------------------------------------------------------------------------------- /patches/src/main/kotlin/li/auna/util/ResourceUtils.kt: -------------------------------------------------------------------------------- 1 | package li.auna.util 2 | 3 | import app.revanced.patcher.patch.PatchException 4 | import app.revanced.patcher.patch.ResourcePatchContext 5 | import app.revanced.patcher.util.Document 6 | import li.auna.util.resource.BaseResource 7 | import org.w3c.dom.Attr 8 | import org.w3c.dom.Element 9 | import org.w3c.dom.Node 10 | import org.w3c.dom.NodeList 11 | import java.io.InputStream 12 | import java.nio.file.Files 13 | import java.nio.file.StandardCopyOption 14 | 15 | private val classLoader = object {}.javaClass.classLoader 16 | 17 | /** 18 | * Returns a sequence for all child nodes. 19 | */ 20 | fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(it) } 21 | 22 | /** 23 | * Returns a sequence for all child nodes. 24 | */ 25 | @Suppress("UNCHECKED_CAST") 26 | fun Node.childElementsSequence() = 27 | this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence 28 | 29 | /** 30 | * Performs the given [action] on each child element. 31 | */ 32 | inline fun Node.forEachChildElement(action: (Element) -> Unit) = 33 | childElementsSequence().forEach { 34 | action(it) 35 | } 36 | 37 | /** 38 | * Recursively traverse the DOM tree starting from the given root node. 39 | * 40 | * @param action function that is called for every node in the tree. 41 | */ 42 | fun Node.doRecursively(action: (Node) -> Unit) { 43 | action(this) 44 | val childNodes = this.childNodes 45 | for (i in 0 until childNodes.length) { 46 | childNodes.item(i).doRecursively(action) 47 | } 48 | } 49 | 50 | fun Node.insertFirst(node: Node) { 51 | if (hasChildNodes()) { 52 | insertBefore(node, firstChild) 53 | } else { 54 | appendChild(node) 55 | } 56 | } 57 | 58 | /** 59 | * Copy resources from the current class loader to the resource directory. 60 | * 61 | * @param sourceResourceDirectory The source resource directory name. 62 | * @param resources The resources to copy. 63 | */ 64 | fun ResourcePatchContext.copyResources( 65 | sourceResourceDirectory: String, 66 | vararg resources: ResourceGroup, 67 | ) { 68 | val targetResourceDirectory = this["res", false] 69 | 70 | for (resourceGroup in resources) { 71 | resourceGroup.resources.forEach { resource -> 72 | val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource" 73 | Files.copy( 74 | inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)!!, 75 | targetResourceDirectory.resolve(resourceFile).toPath(), 76 | StandardCopyOption.REPLACE_EXISTING, 77 | ) 78 | } 79 | } 80 | } 81 | 82 | internal fun inputStreamFromBundledResource( 83 | sourceResourceDirectory: String, 84 | resourceFile: String, 85 | ): InputStream? = classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile") 86 | 87 | /** 88 | * Resource names mapped to their corresponding resource data. 89 | * @param resourceDirectoryName The name of the directory of the resource. 90 | * @param resources A list of resource names. 91 | */ 92 | class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String) 93 | 94 | /** 95 | * Iterate through the children of a node by its tag. 96 | * @param resource The xml resource. 97 | * @param targetTag The target xml node. 98 | * @param callback The callback to call when iterating over the nodes. 99 | */ 100 | fun ResourcePatchContext.iterateXmlNodeChildren( 101 | resource: String, 102 | targetTag: String, 103 | callback: (node: Node) -> Unit, 104 | ) = document(classLoader.getResourceAsStream(resource)!!).use { document -> 105 | val stringsNode = document.getElementsByTagName(targetTag).item(0).childNodes 106 | for (i in 1 until stringsNode.length - 1) callback(stringsNode.item(i)) 107 | } 108 | 109 | /** 110 | * Copies the specified node of the source [Document] to the target [Document]. 111 | * @param source the source [Document]. 112 | * @param target the target [Document]- 113 | * @return AutoCloseable that closes the [Document]s. 114 | */ 115 | fun String.copyXmlNode( 116 | source: Document, 117 | target: Document, 118 | ): AutoCloseable { 119 | val hostNodes = source.getElementsByTagName(this).item(0).childNodes 120 | val destinationNode = target.getElementsByTagName(this).item(0) 121 | 122 | for (index in 0 until hostNodes.length) { 123 | val node = hostNodes.item(index).cloneNode(true) 124 | target.adoptNode(node) 125 | destinationNode.appendChild(node) 126 | } 127 | 128 | return AutoCloseable { 129 | source.close() 130 | target.close() 131 | } 132 | } 133 | 134 | /** 135 | * Add a resource node child. 136 | * 137 | * @param resource The resource to add. 138 | * @param resourceCallback Called when a resource has been processed. 139 | */ 140 | internal fun Node.addResource( 141 | resource: BaseResource, 142 | resourceCallback: (BaseResource) -> Unit = { }, 143 | ) { 144 | appendChild(resource.serialize(ownerDocument, resourceCallback)) 145 | } 146 | 147 | internal fun org.w3c.dom.Document.getNode(tagName: String) = this.getElementsByTagName(tagName).item(0) 148 | 149 | internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? { 150 | for (i in 0 until length) { 151 | val node = item(i) 152 | if (node.nodeType == Node.ELEMENT_NODE) { 153 | val element = node as Element 154 | 155 | if (element.getAttribute(attributeName) == value) { 156 | return element 157 | } 158 | 159 | // Recursively search. 160 | val found = element.childNodes.findElementByAttributeValue(attributeName, value) 161 | if (found != null) { 162 | return found 163 | } 164 | } 165 | } 166 | 167 | return null 168 | } 169 | 170 | internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = 171 | findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value") 172 | 173 | internal fun Element.copyAttributesFrom(oldContainer: Element) { 174 | // Copy attributes from the old element to the new element 175 | val attributes = oldContainer.attributes 176 | for (i in 0 until attributes.length) { 177 | val attr = attributes.item(i) as Attr 178 | setAttribute(attr.name, attr.value) 179 | } 180 | } -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.requests; 2 | 3 | import app.revanced.extension.shared.Utils; 4 | import org.json.JSONArray; 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.net.HttpURLConnection; 13 | import java.net.URL; 14 | 15 | public class Requester { 16 | private Requester() { 17 | } 18 | 19 | public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException { 20 | return getConnectionFromCompiledRoute(apiUrl, route.compile(params)); 21 | } 22 | 23 | public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { 24 | String url = apiUrl + route.getCompiledRoute(); 25 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); 26 | // Request data is in the URL parameters and no body is sent. 27 | // The calling code must set a length if using a request body. 28 | connection.setFixedLengthStreamingMode(0); 29 | connection.setRequestMethod(route.getMethod().name()); 30 | String agentString = System.getProperty("http.agent") 31 | + "; ReVanced/" + Utils.getAppVersionName() 32 | + " (" + Utils.getPatchesReleaseVersion() + ")"; 33 | connection.setRequestProperty("User-Agent", agentString); 34 | 35 | return connection; 36 | } 37 | 38 | /** 39 | * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. 40 | */ 41 | private static String parseInputStreamAndClose(InputStream inputStream) throws IOException { 42 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { 43 | StringBuilder jsonBuilder = new StringBuilder(); 44 | String line; 45 | while ((line = reader.readLine()) != null) { 46 | jsonBuilder.append(line); 47 | jsonBuilder.append('\n'); 48 | } 49 | return jsonBuilder.toString(); 50 | } 51 | } 52 | 53 | /** 54 | * Parse the {@link HttpURLConnection} response as a String. 55 | * This does not close the url connection. If further requests to this host are unlikely 56 | * in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}. 57 | */ 58 | public static String parseString(HttpURLConnection connection) throws IOException { 59 | return parseInputStreamAndClose(connection.getInputStream()); 60 | } 61 | 62 | /** 63 | * Parse the {@link HttpURLConnection} response as a String, and disconnect. 64 | * 65 | * Should only be used if other requests to the server in the near future are unlikely 66 | * 67 | * @see #parseString(HttpURLConnection) 68 | */ 69 | public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException { 70 | String result = parseString(connection); 71 | connection.disconnect(); 72 | return result; 73 | } 74 | 75 | /** 76 | * Parse the {@link HttpURLConnection} error stream as a String. 77 | * If the server sent no error response data, this returns an empty string. 78 | */ 79 | public static String parseErrorString(HttpURLConnection connection) throws IOException { 80 | InputStream errorStream = connection.getErrorStream(); 81 | if (errorStream == null) { 82 | return ""; 83 | } 84 | return parseInputStreamAndClose(errorStream); 85 | } 86 | 87 | /** 88 | * Parse the {@link HttpURLConnection} error stream as a String, and disconnect. 89 | * If the server sent no error response data, this returns an empty string. 90 | * 91 | * Should only be used if other requests to the server are unlikely in the near future. 92 | * 93 | * @see #parseErrorString(HttpURLConnection) 94 | */ 95 | public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException { 96 | String result = parseErrorString(connection); 97 | connection.disconnect(); 98 | return result; 99 | } 100 | 101 | /** 102 | * Parse the {@link HttpURLConnection} response into a JSONObject. 103 | * This does not close the url connection. If further requests to this host are unlikely 104 | * in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}. 105 | */ 106 | public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException { 107 | return new JSONObject(parseString(connection)); 108 | } 109 | 110 | /** 111 | * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. 112 | * 113 | * Should only be used if other requests to the server in the near future are unlikely 114 | * 115 | * @see #parseJSONObject(HttpURLConnection) 116 | */ 117 | public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { 118 | JSONObject object = parseJSONObject(connection); 119 | connection.disconnect(); 120 | return object; 121 | } 122 | 123 | /** 124 | * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. 125 | * This does not close the url connection. If further requests to this host are unlikely 126 | * in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}. 127 | */ 128 | public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException { 129 | return new JSONArray(parseString(connection)); 130 | } 131 | 132 | /** 133 | * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. 134 | * 135 | * Should only be used if other requests to the server in the near future are unlikely 136 | * 137 | * @see #parseJSONArray(HttpURLConnection) 138 | */ 139 | public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { 140 | JSONArray array = parseJSONArray(connection); 141 | connection.disconnect(); 142 | return array; 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared; 2 | 3 | import android.util.Log; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import app.revanced.extension.shared.settings.BaseSettings; 7 | 8 | import java.io.PrintWriter; 9 | import java.io.StringWriter; 10 | 11 | import static app.revanced.extension.shared.settings.BaseSettings.*; 12 | 13 | public class Logger { 14 | 15 | /** 16 | * Log messages using lambdas. 17 | */ 18 | @FunctionalInterface 19 | public interface LogMessage { 20 | @NonNull 21 | String buildMessageString(); 22 | 23 | /** 24 | * @return For outer classes, this returns {@link Class#getSimpleName()}. 25 | * For static, inner, or anonymous classes, this returns the simple name of the enclosing class. 26 | *
27 | * For example, each of these classes return 'SomethingView': 28 | * 29 | * com.company.SomethingView 30 | * com.company.SomethingView$StaticClass 31 | * com.company.SomethingView$1 32 | * 33 | */ 34 | private String findOuterClassSimpleName() { 35 | var selfClass = this.getClass(); 36 | 37 | String fullClassName = selfClass.getName(); 38 | final int dollarSignIndex = fullClassName.indexOf('$'); 39 | if (dollarSignIndex < 0) { 40 | return selfClass.getSimpleName(); // Already an outer class. 41 | } 42 | 43 | // Class is inner, static, or anonymous. 44 | // Parse the simple name full name. 45 | // A class with no package returns index of -1, but incrementing gives index zero which is correct. 46 | final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1; 47 | return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex); 48 | } 49 | } 50 | 51 | private static final String REVANCED_LOG_PREFIX = "revanced: "; 52 | 53 | /** 54 | * Logs debug messages under the outer class name of the code calling this method. 55 | * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} 56 | * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. 57 | */ 58 | public static void printDebug(@NonNull LogMessage message) { 59 | printDebug(message, null); 60 | } 61 | 62 | /** 63 | * Logs debug messages under the outer class name of the code calling this method. 64 | * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} 65 | * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. 66 | */ 67 | public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) { 68 | if (DEBUG.get()) { 69 | String logMessage = message.buildMessageString(); 70 | String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); 71 | 72 | if (DEBUG_STACKTRACE.get()) { 73 | var builder = new StringBuilder(logMessage); 74 | var sw = new StringWriter(); 75 | new Throwable().printStackTrace(new PrintWriter(sw)); 76 | 77 | builder.append('\n').append(sw); 78 | logMessage = builder.toString(); 79 | } 80 | 81 | if (ex == null) { 82 | Log.d(logTag, logMessage); 83 | } else { 84 | Log.d(logTag, logMessage, ex); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Logs information messages using the outer class name of the code calling this method. 91 | */ 92 | public static void printInfo(@NonNull LogMessage message) { 93 | printInfo(message, null); 94 | } 95 | 96 | /** 97 | * Logs information messages using the outer class name of the code calling this method. 98 | */ 99 | public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) { 100 | String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); 101 | String logMessage = message.buildMessageString(); 102 | if (ex == null) { 103 | Log.i(logTag, logMessage); 104 | } else { 105 | Log.i(logTag, logMessage, ex); 106 | } 107 | } 108 | 109 | /** 110 | * Logs exceptions under the outer class name of the code calling this method. 111 | */ 112 | public static void printException(@NonNull LogMessage message) { 113 | printException(message, null); 114 | } 115 | 116 | /** 117 | * Logs exceptions under the outer class name of the code calling this method. 118 | *

119 | * If the calling code is showing it's own error toast, 120 | * instead use {@link #printInfo(LogMessage, Exception)} 121 | * 122 | * @param message log message 123 | * @param ex exception (optional) 124 | */ 125 | public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) { 126 | String messageString = message.buildMessageString(); 127 | String outerClassSimpleName = message.findOuterClassSimpleName(); 128 | String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName; 129 | if (ex == null) { 130 | Log.e(logMessage, messageString); 131 | } else { 132 | Log.e(logMessage, messageString, ex); 133 | } 134 | if (DEBUG_TOAST_ON_ERROR.get()) { 135 | Utils.showToastLong(outerClassSimpleName + ": " + messageString); 136 | } 137 | } 138 | 139 | /** 140 | * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. 141 | * Normally this method should not be used. 142 | */ 143 | public static void initializationInfo(@NonNull Class callingClass, @NonNull String message) { 144 | Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message); 145 | } 146 | 147 | /** 148 | * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. 149 | * Normally this method should not be used. 150 | */ 151 | public static void initializationException(@NonNull Class callingClass, @NonNull String message, 152 | @Nullable Exception ex) { 153 | Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex); 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.checks; 2 | 3 | import static android.text.Html.FROM_HTML_MODE_COMPACT; 4 | import static app.revanced.extension.shared.StringRef.str; 5 | import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction; 6 | 7 | import android.annotation.SuppressLint; 8 | import android.app.Activity; 9 | import android.app.AlertDialog; 10 | import android.content.DialogInterface; 11 | import android.content.Intent; 12 | import android.net.Uri; 13 | import android.text.Html; 14 | import android.widget.Button; 15 | 16 | import androidx.annotation.Nullable; 17 | 18 | import java.util.Collection; 19 | 20 | import app.revanced.extension.shared.Logger; 21 | import app.revanced.extension.shared.Utils; 22 | import app.revanced.extension.shared.settings.BaseSettings; 23 | 24 | abstract class Check { 25 | private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2; 26 | 27 | private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15; 28 | private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10; 29 | 30 | private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app"); 31 | 32 | /** 33 | * @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed. 34 | */ 35 | @Nullable 36 | protected abstract Boolean check(); 37 | 38 | protected abstract String failureReason(); 39 | 40 | /** 41 | * Specifies a sorting order for displaying the checks that failed. 42 | * A lower value indicates to show first before other checks. 43 | */ 44 | public abstract int uiSortingValue(); 45 | 46 | /** 47 | * For debugging and development only. 48 | * Forces all checks to be performed and the check failed dialog to be shown. 49 | * Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED} 50 | * set to -1. 51 | */ 52 | static boolean debugAlwaysShowWarning() { 53 | final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0; 54 | if (alwaysShowWarning) { 55 | Logger.printInfo(() -> "Debug forcing environment check warning to show"); 56 | } 57 | 58 | return alwaysShowWarning; 59 | } 60 | 61 | static boolean shouldRun() { 62 | return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() 63 | < NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING; 64 | } 65 | 66 | static void disableForever() { 67 | Logger.printInfo(() -> "Environment checks disabled forever"); 68 | 69 | BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE); 70 | } 71 | 72 | @SuppressLint("NewApi") 73 | static void issueWarning(Activity activity, Collection failedChecks) { 74 | final var reasons = new StringBuilder(); 75 | 76 | reasons.append("

    "); 77 | for (var check : failedChecks) { 78 | // Add a non breaking space to fix bullet points spacing issue. 79 | reasons.append("
  •  ").append(check.failureReason()); 80 | } 81 | reasons.append("
"); 82 | 83 | var message = Html.fromHtml( 84 | str("revanced_check_environment_failed_message", reasons.toString()), 85 | FROM_HTML_MODE_COMPACT 86 | ); 87 | 88 | Utils.runOnMainThreadDelayed(() -> { 89 | AlertDialog alert = new AlertDialog.Builder(activity) 90 | .setCancelable(false) 91 | .setIconAttribute(android.R.attr.alertDialogIcon) 92 | .setTitle(str("revanced_check_environment_failed_title")) 93 | .setMessage(message) 94 | .setPositiveButton( 95 | " ", 96 | (dialog, which) -> { 97 | final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE); 98 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 99 | activity.startActivity(intent); 100 | 101 | // Shutdown to prevent the user from navigating back to this app, 102 | // which is no longer showing a warning dialog. 103 | activity.finishAffinity(); 104 | System.exit(0); 105 | } 106 | ).setNegativeButton( 107 | " ", 108 | (dialog, which) -> { 109 | // Cleanup data if the user incorrectly imported a huge negative number. 110 | final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()); 111 | BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1); 112 | 113 | dialog.dismiss(); 114 | } 115 | ).create(); 116 | 117 | Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() { 118 | boolean hasRun; 119 | @Override 120 | public void onStart(AlertDialog dialog) { 121 | // Only run this once, otherwise if the user changes to a different app 122 | // then changes back, this handler will run again and disable the buttons. 123 | if (hasRun) { 124 | return; 125 | } 126 | hasRun = true; 127 | 128 | var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); 129 | openWebsiteButton.setEnabled(false); 130 | 131 | var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); 132 | dismissButton.setEnabled(false); 133 | 134 | getCountdownRunnable(dismissButton, openWebsiteButton).run(); 135 | } 136 | }); 137 | }, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs. 138 | } 139 | 140 | private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) { 141 | return new Runnable() { 142 | private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON; 143 | 144 | @Override 145 | public void run() { 146 | Utils.verifyOnMainThread(); 147 | 148 | if (secondsRemaining > 0) { 149 | if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) { 150 | openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button")); 151 | openWebsiteButton.setEnabled(true); 152 | } 153 | 154 | secondsRemaining--; 155 | 156 | Utils.runOnMainThreadDelayed(this, 1000); 157 | } else { 158 | dismissButton.setText(str("revanced_check_environment_dialog_ignore_button")); 159 | dismissButton.setEnabled(true); 160 | } 161 | } 162 | }; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.settings.preference; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceFragment; 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | import app.revanced.extension.shared.Logger; 9 | import app.revanced.extension.shared.Utils; 10 | 11 | import java.util.Objects; 12 | 13 | /** 14 | * Shared categories, and helper methods. 15 | * 16 | * The various save methods store numbers as Strings, 17 | * which is required if using {@link PreferenceFragment}. 18 | * 19 | * If saved numbers will not be used with a preference fragment, 20 | * then store the primitive numbers using the {@link #preferences} itself. 21 | */ 22 | public class SharedPrefCategory { 23 | @NonNull 24 | public final String name; 25 | @NonNull 26 | public final SharedPreferences preferences; 27 | 28 | public SharedPrefCategory(@NonNull String name) { 29 | this.name = Objects.requireNonNull(name); 30 | preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE); 31 | } 32 | 33 | private void removeConflictingPreferenceKeyValue(@NonNull String key) { 34 | Logger.printException(() -> "Found conflicting preference: " + key); 35 | removeKey(key); 36 | } 37 | 38 | private void saveObjectAsString(@NonNull String key, @Nullable Object value) { 39 | preferences.edit().putString(key, (value == null ? null : value.toString())).apply(); 40 | } 41 | 42 | /** 43 | * Removes any preference data type that has the specified key. 44 | */ 45 | public void removeKey(@NonNull String key) { 46 | preferences.edit().remove(Objects.requireNonNull(key)).apply(); 47 | } 48 | 49 | public void saveBoolean(@NonNull String key, boolean value) { 50 | preferences.edit().putBoolean(key, value).apply(); 51 | } 52 | 53 | /** 54 | * @param value a NULL parameter removes the value from the preferences 55 | */ 56 | public void saveEnumAsString(@NonNull String key, @Nullable Enum value) { 57 | saveObjectAsString(key, value); 58 | } 59 | 60 | /** 61 | * @param value a NULL parameter removes the value from the preferences 62 | */ 63 | public void saveIntegerString(@NonNull String key, @Nullable Integer value) { 64 | saveObjectAsString(key, value); 65 | } 66 | 67 | /** 68 | * @param value a NULL parameter removes the value from the preferences 69 | */ 70 | public void saveLongString(@NonNull String key, @Nullable Long value) { 71 | saveObjectAsString(key, value); 72 | } 73 | 74 | /** 75 | * @param value a NULL parameter removes the value from the preferences 76 | */ 77 | public void saveFloatString(@NonNull String key, @Nullable Float value) { 78 | saveObjectAsString(key, value); 79 | } 80 | 81 | /** 82 | * @param value a NULL parameter removes the value from the preferences 83 | */ 84 | public void saveString(@NonNull String key, @Nullable String value) { 85 | saveObjectAsString(key, value); 86 | } 87 | 88 | @NonNull 89 | public String getString(@NonNull String key, @NonNull String _default) { 90 | Objects.requireNonNull(_default); 91 | try { 92 | return preferences.getString(key, _default); 93 | } catch (ClassCastException ex) { 94 | // Value stored is a completely different type (should never happen). 95 | removeConflictingPreferenceKeyValue(key); 96 | return _default; 97 | } 98 | } 99 | 100 | @NonNull 101 | public > T getEnum(@NonNull String key, @NonNull T _default) { 102 | Objects.requireNonNull(_default); 103 | try { 104 | String enumName = preferences.getString(key, null); 105 | if (enumName != null) { 106 | try { 107 | // noinspection unchecked 108 | return (T) Enum.valueOf(_default.getClass(), enumName); 109 | } catch (IllegalArgumentException ex) { 110 | // Info level to allow removing enum values in the future without showing any user errors. 111 | Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName); 112 | removeKey(key); 113 | } 114 | } 115 | } catch (ClassCastException ex) { 116 | // Value stored is a completely different type (should never happen). 117 | removeConflictingPreferenceKeyValue(key); 118 | } 119 | return _default; 120 | } 121 | 122 | public boolean getBoolean(@NonNull String key, boolean _default) { 123 | try { 124 | return preferences.getBoolean(key, _default); 125 | } catch (ClassCastException ex) { 126 | // Value stored is a completely different type (should never happen). 127 | removeConflictingPreferenceKeyValue(key); 128 | return _default; 129 | } 130 | } 131 | 132 | @NonNull 133 | public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) { 134 | try { 135 | String value = preferences.getString(key, null); 136 | if (value != null) { 137 | return Integer.valueOf(value); 138 | } 139 | } catch (ClassCastException | NumberFormatException ex) { 140 | try { 141 | // Old data previously stored as primitive. 142 | return preferences.getInt(key, _default); 143 | } catch (ClassCastException ex2) { 144 | // Value stored is a completely different type (should never happen). 145 | removeConflictingPreferenceKeyValue(key); 146 | } 147 | } 148 | return _default; 149 | } 150 | 151 | @NonNull 152 | public Long getLongString(@NonNull String key, @NonNull Long _default) { 153 | try { 154 | String value = preferences.getString(key, null); 155 | if (value != null) { 156 | return Long.valueOf(value); 157 | } 158 | } catch (ClassCastException | NumberFormatException ex) { 159 | try { 160 | return preferences.getLong(key, _default); 161 | } catch (ClassCastException ex2) { 162 | removeConflictingPreferenceKeyValue(key); 163 | } 164 | } 165 | return _default; 166 | } 167 | 168 | @NonNull 169 | public Float getFloatString(@NonNull String key, @NonNull Float _default) { 170 | try { 171 | String value = preferences.getString(key, null); 172 | if (value != null) { 173 | return Float.valueOf(value); 174 | } 175 | } catch (ClassCastException | NumberFormatException ex) { 176 | try { 177 | return preferences.getFloat(key, _default); 178 | } catch (ClassCastException ex2) { 179 | removeConflictingPreferenceKeyValue(key); 180 | } 181 | } 182 | return _default; 183 | } 184 | 185 | @NonNull 186 | @Override 187 | public String toString() { 188 | return name; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java: -------------------------------------------------------------------------------- 1 | package app.revanced.extension.shared.fixes.slink; 2 | 3 | 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import androidx.annotation.NonNull; 9 | import app.revanced.extension.shared.Logger; 10 | import app.revanced.extension.shared.Utils; 11 | 12 | import java.io.IOException; 13 | import java.net.HttpURLConnection; 14 | import java.net.SocketTimeoutException; 15 | import java.net.URL; 16 | import java.util.Objects; 17 | 18 | import static app.revanced.extension.shared.Utils.getContext; 19 | 20 | 21 | /** 22 | * Base class to implement /s/ link resolution in 3rd party Reddit apps. 23 | *
24 | *
25 | * Usage: 26 | *
27 | *
28 | * An implementation of this class must have two static methods that are called by the app: 29 | *
    30 | *
  • public static boolean patchResolveSLink(String link)
  • 31 | *
  • public static void patchSetAccessToken(String accessToken)
  • 32 | *
33 | * The static methods must call the instance methods of the base class. 34 | *
35 | * The singleton pattern can be used to access the instance of the class: 36 | *
 37 |  * {@code
 38 |  * {
 39 |  *     INSTANCE = new FixSLinksPatch();
 40 |  * }
 41 |  * }
 42 |  * 
43 | * Set the app's web view activity class as a fallback to open /s/ links if the resolution fails: 44 | *
 45 |  * {@code
 46 |  * private FixSLinksPatch() {
 47 |  *     webViewActivityClass = WebViewActivity.class;
 48 |  * }
 49 |  * }
 50 |  * 
51 | * Hook the app's navigation handler to call this method before doing any of its own resolution: 52 | *
 53 |  * {@code
 54 |  * public static boolean patchResolveSLink(Context context, String link) {
 55 |  *     return INSTANCE.resolveSLink(context, link);
 56 |  * }
 57 |  * }
 58 |  * 
59 | * If this method returns true, the app should early return and not do any of its own resolution. 60 | *
61 | *
62 | * Hook the app's access token so that this class can use it to resolve /s/ links: 63 | *
 64 |  * {@code
 65 |  * public static void patchSetAccessToken(String accessToken) {
 66 |  *     INSTANCE.setAccessToken(access_token);
 67 |  * }
 68 |  * }
 69 |  * 
70 | */ 71 | public abstract class BaseFixSLinksPatch { 72 | /** 73 | * The class of the activity used to open links in a web view if resolving them fails. 74 | */ 75 | protected Class webViewActivityClass; 76 | 77 | /** 78 | * The access token used to resolve the /s/ link. 79 | */ 80 | protected String accessToken; 81 | 82 | /** 83 | * The URL that was trying to be resolved before the access token was set. 84 | * If this is not null, the URL will be resolved right after the access token is set. 85 | */ 86 | protected String pendingUrl; 87 | 88 | /** 89 | * The singleton instance of the class. 90 | */ 91 | protected static BaseFixSLinksPatch INSTANCE; 92 | 93 | public boolean resolveSLink(String link) { 94 | switch (resolveLink(link)) { 95 | case ACCESS_TOKEN_START: { 96 | pendingUrl = link; 97 | return true; 98 | } 99 | case DO_NOTHING: 100 | return true; 101 | default: 102 | return false; 103 | } 104 | } 105 | 106 | private ResolveResult resolveLink(String link) { 107 | Context context = getContext(); 108 | if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) { 109 | // A link ends with #bypass if it failed to resolve below. 110 | // resolveLink is called with the same link again but this time with #bypass 111 | // so that the link is opened in the app browser instead of trying to resolve it again. 112 | if (link.endsWith("#bypass")) { 113 | openInAppBrowser(context, link); 114 | 115 | return ResolveResult.DO_NOTHING; 116 | } 117 | 118 | Logger.printDebug(() -> "Resolving " + link); 119 | 120 | if (accessToken == null) { 121 | // This is not optimal. 122 | // However, an accessToken is necessary to make an authenticated request to Reddit. 123 | // in case Reddit has banned the IP - e.g. VPN. 124 | Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); 125 | context.startActivity(startIntent); 126 | 127 | return ResolveResult.ACCESS_TOKEN_START; 128 | } 129 | 130 | 131 | Utils.runOnBackgroundThread(() -> { 132 | String bypassLink = link + "#bypass"; 133 | 134 | String finalLocation = bypassLink; 135 | try { 136 | HttpURLConnection connection = getHttpURLConnection(link, accessToken); 137 | connection.connect(); 138 | String location = connection.getHeaderField("location"); 139 | connection.disconnect(); 140 | 141 | Objects.requireNonNull(location, "Location is null"); 142 | 143 | finalLocation = location; 144 | Logger.printDebug(() -> "Resolved " + link + " to " + location); 145 | } catch (SocketTimeoutException e) { 146 | Logger.printException(() -> "Timeout when trying to resolve " + link, e); 147 | finalLocation = bypassLink; 148 | } catch (Exception e) { 149 | Logger.printException(() -> "Failed to resolve " + link, e); 150 | finalLocation = bypassLink; 151 | } finally { 152 | Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation)); 153 | startIntent.setPackage(context.getPackageName()); 154 | startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 155 | context.startActivity(startIntent); 156 | } 157 | }); 158 | 159 | return ResolveResult.DO_NOTHING; 160 | } 161 | 162 | return ResolveResult.CONTINUE; 163 | } 164 | 165 | public void setAccessToken(String accessToken) { 166 | Logger.printDebug(() -> "Setting access token"); 167 | 168 | this.accessToken = accessToken; 169 | 170 | // In case a link was trying to be resolved before access token was set. 171 | // The link is resolved now, after the access token is set. 172 | if (pendingUrl != null) { 173 | String link = pendingUrl; 174 | pendingUrl = null; 175 | 176 | Logger.printDebug(() -> "Opening pending URL"); 177 | 178 | resolveLink(link); 179 | } 180 | } 181 | 182 | private void openInAppBrowser(Context context, String link) { 183 | Intent intent = new Intent(context, webViewActivityClass); 184 | intent.putExtra("url", link); 185 | context.startActivity(intent); 186 | } 187 | 188 | @NonNull 189 | private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException { 190 | URL url = new URL(link); 191 | 192 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 193 | connection.setInstanceFollowRedirects(false); 194 | connection.setRequestMethod("HEAD"); 195 | connection.setConnectTimeout(2000); 196 | connection.setReadTimeout(2000); 197 | 198 | if (accessToken != null) { 199 | Logger.printDebug(() -> "Setting access token to make /s/ request"); 200 | 201 | connection.setRequestProperty("Authorization", "Bearer " + accessToken); 202 | } else { 203 | Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null"); 204 | } 205 | 206 | return connection; 207 | } 208 | } 209 | --------------------------------------------------------------------------------