,
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 extends Activity> 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 |
--------------------------------------------------------------------------------