mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-12-30 14:45:48 +01:00
feat(twitch): integrations code for patches (#216)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
24367cea3f
commit
d4c3b74a9a
@ -45,4 +45,5 @@ android {
|
||||
dependencies {
|
||||
compileOnly(project(mapOf("path" to ":dummy")))
|
||||
compileOnly("androidx.annotation:annotation:1.5.0")
|
||||
compileOnly("androidx.appcompat:appcompat:1.5.1")
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package app.revanced.twitch.settings;
|
||||
|
||||
public enum ReturnType {
|
||||
BOOLEAN, INTEGER, STRING, LONG, FLOAT
|
||||
}
|
149
app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java
Normal file
149
app/src/main/java/app/revanced/twitch/settings/SettingsEnum.java
Normal file
@ -0,0 +1,149 @@
|
||||
package app.revanced.twitch.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import app.revanced.twitch.utils.LogHelper;
|
||||
import app.revanced.twitch.utils.ReVancedUtils;
|
||||
|
||||
public enum SettingsEnum {
|
||||
/* Ads */
|
||||
BLOCK_VIDEO_ADS("revanced_block_video_ads", true, ReturnType.BOOLEAN),
|
||||
BLOCK_AUDIO_ADS("revanced_block_audio_ads", true, ReturnType.BOOLEAN),
|
||||
|
||||
/* Chat */
|
||||
SHOW_DELETED_MESSAGES("revanced_show_deleted_messages", "cross-out", ReturnType.STRING),
|
||||
|
||||
/* Misc */
|
||||
DEBUG_MODE("revanced_debug_mode", false, ReturnType.BOOLEAN, true);
|
||||
|
||||
public static final String REVANCED_PREFS = "revanced_prefs";
|
||||
|
||||
private final String path;
|
||||
private final Object defaultValue;
|
||||
private final ReturnType returnType;
|
||||
private final boolean rebootApp;
|
||||
|
||||
private Object value = null;
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.returnType = returnType;
|
||||
this.rebootApp = false;
|
||||
}
|
||||
|
||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType, Boolean rebootApp) {
|
||||
this.path = path;
|
||||
this.defaultValue = defaultValue;
|
||||
this.returnType = returnType;
|
||||
this.rebootApp = rebootApp;
|
||||
}
|
||||
|
||||
static {
|
||||
load();
|
||||
}
|
||||
|
||||
private static void load() {
|
||||
ReVancedUtils.ifContextAttached((context -> {
|
||||
try {
|
||||
SharedPreferences prefs = context.getSharedPreferences(REVANCED_PREFS, Context.MODE_PRIVATE);
|
||||
for (SettingsEnum setting : values()) {
|
||||
Object value = setting.getDefaultValue();
|
||||
|
||||
try {
|
||||
switch (setting.getReturnType()) {
|
||||
case BOOLEAN:
|
||||
value = prefs.getBoolean(setting.getPath(), (boolean)setting.getDefaultValue());
|
||||
break;
|
||||
// Numbers are implicitly converted from strings
|
||||
case FLOAT:
|
||||
case LONG:
|
||||
case INTEGER:
|
||||
case STRING:
|
||||
value = prefs.getString(setting.getPath(), setting.getDefaultValue() + "");
|
||||
break;
|
||||
default:
|
||||
LogHelper.error("Setting '%s' does not have a valid type", setting.name());
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (ClassCastException ex) {
|
||||
LogHelper.printException("Failed to read value", ex);
|
||||
}
|
||||
|
||||
setting.setValue(value);
|
||||
LogHelper.debug("Loaded setting '%s' with value %s", setting.name(), value);
|
||||
}
|
||||
} catch (Throwable th) {
|
||||
LogHelper.printException("Failed to load settings", th);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void setValue(Object newValue) {
|
||||
// Implicitly convert strings to numbers depending on the ResultType
|
||||
switch (returnType) {
|
||||
case FLOAT:
|
||||
value = Float.valueOf(newValue + "");
|
||||
break;
|
||||
case LONG:
|
||||
value = Long.valueOf(newValue + "");
|
||||
break;
|
||||
case INTEGER:
|
||||
value = Integer.valueOf(newValue + "");
|
||||
break;
|
||||
default:
|
||||
value = newValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void saveValue(Object newValue) {
|
||||
ReVancedUtils.ifContextAttached((context) -> {
|
||||
SharedPreferences prefs = context.getSharedPreferences(REVANCED_PREFS, Context.MODE_PRIVATE);
|
||||
if (returnType == ReturnType.BOOLEAN) {
|
||||
prefs.edit().putBoolean(path, (Boolean)newValue).apply();
|
||||
} else {
|
||||
prefs.edit().putString(path, newValue + "").apply();
|
||||
}
|
||||
value = newValue;
|
||||
});
|
||||
}
|
||||
|
||||
public int getInt() {
|
||||
return (int) value;
|
||||
}
|
||||
|
||||
public String getString() {
|
||||
return (String) value;
|
||||
}
|
||||
|
||||
public boolean getBoolean() {
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
public Long getLong() {
|
||||
return (Long) value;
|
||||
}
|
||||
|
||||
public Float getFloat() {
|
||||
return (Float) value;
|
||||
}
|
||||
|
||||
public Object getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public ReturnType getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
public boolean shouldRebootOnChange() {
|
||||
return rebootApp;
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package app.revanced.twitch.settingsmenu;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.SwitchPreference;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.twitch.settings.SettingsEnum;
|
||||
import app.revanced.twitch.utils.LogHelper;
|
||||
import app.revanced.twitch.utils.ReVancedUtils;
|
||||
|
||||
import tv.twitch.android.app.core.LandingActivity;
|
||||
|
||||
public class ReVancedSettingsFragment extends PreferenceFragment {
|
||||
|
||||
private boolean registered = false;
|
||||
private boolean settingsInitialized = false;
|
||||
|
||||
SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, key) -> {
|
||||
LogHelper.debug("Setting '%s' changed", key);
|
||||
syncPreference(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync preference
|
||||
* @param key Preference to load. If key is null, all preferences are updated
|
||||
*/
|
||||
private void syncPreference(@Nullable String key) {
|
||||
for (SettingsEnum setting : SettingsEnum.values()) {
|
||||
if (!setting.getPath().equals(key) && key != null)
|
||||
continue;
|
||||
|
||||
Preference pref = this.findPreference(setting.getPath());
|
||||
LogHelper.debug("Syncing setting '%s' with UI", setting.getPath());
|
||||
|
||||
if (pref instanceof SwitchPreference) {
|
||||
setting.setValue(((SwitchPreference) pref).isChecked());
|
||||
}
|
||||
else if (pref instanceof EditTextPreference) {
|
||||
setting.setValue(((EditTextPreference) pref).getText());
|
||||
}
|
||||
else if (pref instanceof ListPreference) {
|
||||
ListPreference listPref = (ListPreference) pref;
|
||||
listPref.setSummary(listPref.getEntry());
|
||||
setting.setValue(listPref.getValue());
|
||||
}
|
||||
else {
|
||||
LogHelper.error("Setting '%s' cannot be handled!", pref);
|
||||
}
|
||||
|
||||
if (ReVancedUtils.getContext() != null && key != null && settingsInitialized && setting.shouldRebootOnChange()) {
|
||||
rebootDialog(getActivity());
|
||||
}
|
||||
|
||||
// First onChange event is caused by initial state loading
|
||||
this.settingsInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
try {
|
||||
PreferenceManager mgr = getPreferenceManager();
|
||||
mgr.setSharedPreferencesName(SettingsEnum.REVANCED_PREFS);
|
||||
mgr.getSharedPreferences().registerOnSharedPreferenceChangeListener(this.listener);
|
||||
|
||||
addPreferencesFromResource(
|
||||
getResources().getIdentifier(
|
||||
SettingsEnum.REVANCED_PREFS,
|
||||
"xml",
|
||||
this.getContext().getPackageName()
|
||||
)
|
||||
);
|
||||
|
||||
// Sync all preferences with UI
|
||||
syncPreference(null);
|
||||
|
||||
this.registered = true;
|
||||
} catch (Throwable th) {
|
||||
LogHelper.printException("Error during onCreate()", th);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (this.registered) {
|
||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this.listener);
|
||||
this.registered = false;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private void reboot(Activity activity) {
|
||||
int flags;
|
||||
flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
((AlarmManager) activity.getSystemService(Context.ALARM_SERVICE)).setExact(AlarmManager.ELAPSED_REALTIME, 1500L, PendingIntent.getActivity(activity, 0, new Intent(activity, LandingActivity.class), flags));
|
||||
Process.killProcess(Process.myPid());
|
||||
}
|
||||
|
||||
private void rebootDialog(final Activity activity) {
|
||||
new AlertDialog.Builder(activity).
|
||||
setMessage(ReVancedUtils.getString("revanced_reboot_message")).
|
||||
setPositiveButton(ReVancedUtils.getString("revanced_reboot"), (dialog, i) -> reboot(activity))
|
||||
.setNegativeButton(ReVancedUtils.getString("revanced_cancel"), null)
|
||||
.show();
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package app.revanced.twitch.settingsmenu;
|
||||
|
||||
import static app.revanced.twitch.utils.ReVancedUtils.getIdentifier;
|
||||
import static app.revanced.twitch.utils.ReVancedUtils.getStringId;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.twitch.utils.ReVancedUtils;
|
||||
import app.revanced.twitch.utils.LogHelper;
|
||||
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
|
||||
import tv.twitch.android.settings.SettingsActivity;
|
||||
|
||||
public class SettingsHooks {
|
||||
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
|
||||
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
|
||||
|
||||
/**
|
||||
* Launches SettingsActivity and show ReVanced settings
|
||||
*/
|
||||
public static void startSettingsActivity() {
|
||||
LogHelper.debug("Launching ReVanced settings");
|
||||
|
||||
ReVancedUtils.ifContextAttached((c) -> {
|
||||
Intent intent = new Intent(c, SettingsActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(EXTRA_REVANCED_SETTINGS, true);
|
||||
intent.putExtras(bundle);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
c.startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for easy access in smali
|
||||
* @return Returns string resource id
|
||||
*/
|
||||
public static int getReVancedSettingsString() {
|
||||
return getStringId("revanced_settings");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated
|
||||
* @return Returns a modified list of menu groups
|
||||
*/
|
||||
public static List<SettingsMenuGroup> handleSettingMenuCreation(List<SettingsMenuGroup> settingGroups, Object revancedEntry) {
|
||||
List<SettingsMenuGroup> groups = new ArrayList<>(settingGroups);
|
||||
|
||||
if(groups.size() < 1) {
|
||||
// Create new menu group if none exist yet
|
||||
List<Object> items = new ArrayList<>();
|
||||
items.add(revancedEntry);
|
||||
groups.add(new SettingsMenuGroup(items));
|
||||
}
|
||||
else {
|
||||
// Add to last menu group
|
||||
int groupIdx = groups.size() - 1;
|
||||
List<Object> items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
|
||||
items.add(revancedEntry);
|
||||
groups.add(new SettingsMenuGroup(items));
|
||||
}
|
||||
|
||||
LogHelper.debug("%d menu groups in list", settingGroups.size());
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts settings menu group onclick events
|
||||
* @return Returns true if handled, otherwise false
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static boolean handleSettingMenuOnClick(Enum item) {
|
||||
LogHelper.debug("item %d clicked", item.ordinal());
|
||||
if(item.ordinal() != REVANCED_SETTINGS_MENU_ITEM_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
startSettingsActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts fragment loading in SettingsActivity.onCreate
|
||||
* @return Returns true if the revanced settings have been requested by the user, otherwise false
|
||||
*/
|
||||
public static boolean handleSettingsCreation(AppCompatActivity base) {
|
||||
if(!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
|
||||
LogHelper.debug("Revanced settings not requested");
|
||||
return false; // User wants to enter another settings fragment
|
||||
}
|
||||
LogHelper.debug("ReVanced settings requested");
|
||||
|
||||
ReVancedSettingsFragment fragment = new ReVancedSettingsFragment();
|
||||
ActionBar supportActionBar = base.getSupportActionBar();
|
||||
if(supportActionBar != null)
|
||||
supportActionBar.setTitle(getStringId("revanced_settings"));
|
||||
|
||||
base.getFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(getIdentifier("fragment_container", "id"), fragment)
|
||||
.commit();
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package app.revanced.twitch.settingsmenu.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class CustomPreferenceCategory extends PreferenceCategory {
|
||||
public CustomPreferenceCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindView(View rootView) {
|
||||
super.onBindView(rootView);
|
||||
|
||||
if(rootView instanceof TextView) {
|
||||
((TextView) rootView).setTextColor(Color.parseColor("#8161b3"));
|
||||
}
|
||||
}
|
||||
}
|
49
app/src/main/java/app/revanced/twitch/utils/LogHelper.java
Normal file
49
app/src/main/java/app/revanced/twitch/utils/LogHelper.java
Normal file
@ -0,0 +1,49 @@
|
||||
package app.revanced.twitch.utils;
|
||||
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import app.revanced.twitch.settings.SettingsEnum;
|
||||
|
||||
public class LogHelper {
|
||||
|
||||
public static String getCallOrigin()
|
||||
{
|
||||
try {
|
||||
final StackTraceElement elem = new Throwable().getStackTrace()[/* depth */ 2];
|
||||
final String fullName = elem.getClassName();
|
||||
return fullName.substring(fullName.lastIndexOf('.') + 1) + "/" + elem.getMethodName();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return "<unknown>";
|
||||
}
|
||||
}
|
||||
|
||||
public static final String TAG = "revanced";
|
||||
|
||||
public static void debug(String message, Object... args) {
|
||||
Log.d(TAG, getCallOrigin() + ": " + String.format(message, args));
|
||||
}
|
||||
|
||||
public static void info(String message, Object... args) {
|
||||
Log.i(TAG, getCallOrigin() + ": " + String.format(message, args));
|
||||
}
|
||||
|
||||
public static void error(String message, Object... args) {
|
||||
String msg = getCallOrigin() + ": " + String.format(message, args);
|
||||
showDebugToast(msg);
|
||||
Log.e(TAG, msg);
|
||||
}
|
||||
|
||||
public static void printException(String message, Throwable ex) {
|
||||
String msg = getCallOrigin() + ": " + message;
|
||||
showDebugToast(msg + " (" + ex.getClass().getSimpleName() + ")");
|
||||
Log.e(TAG, msg, ex);
|
||||
}
|
||||
|
||||
private static void showDebugToast(String msg) {
|
||||
if(SettingsEnum.DEBUG_MODE.getBoolean()) {
|
||||
ReVancedUtils.ifContextAttached((c) -> Toast.makeText(c, msg, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package app.revanced.twitch.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
public class ReVancedUtils {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static Context context;
|
||||
|
||||
/**
|
||||
* Regular context getter
|
||||
* @return Returns context or null if not initialized
|
||||
*/
|
||||
public static Context getContext() {
|
||||
if (context != null) {
|
||||
return context;
|
||||
}
|
||||
|
||||
LogHelper.error("Context is null (at %s)", LogHelper.getCallOrigin());
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute lambda only if context attached.
|
||||
*/
|
||||
public static void ifContextAttached(SafeContextAccessLambda lambda) {
|
||||
if (context != null) {
|
||||
lambda.run(context);
|
||||
return;
|
||||
}
|
||||
|
||||
LogHelper.error("Context is null, lambda not executed (at %s)", LogHelper.getCallOrigin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute lambda only if context attached.
|
||||
* @return Returns result on success or valueOnError on failure
|
||||
*/
|
||||
public static <T> T ifContextAttached(SafeContextAccessReturnLambda<T> lambda, T valueOnError) {
|
||||
if (context != null) {
|
||||
return lambda.run(context);
|
||||
}
|
||||
|
||||
LogHelper.error("Context is null, lambda not executed (at %s)", LogHelper.getCallOrigin());
|
||||
return valueOnError;
|
||||
}
|
||||
|
||||
public interface SafeContextAccessReturnLambda<T> {
|
||||
T run(Context ctx);
|
||||
}
|
||||
|
||||
public interface SafeContextAccessLambda {
|
||||
void run(Context ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource id safely
|
||||
* @return May return 0 if resource not found or context not attached
|
||||
*/
|
||||
@SuppressLint("DiscouragedApi")
|
||||
public static int getIdentifier(String name, String defType) {
|
||||
return ifContextAttached(
|
||||
(context) -> {
|
||||
int resId = context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||
if(resId == 0) {
|
||||
LogHelper.error("Resource '%s' not found (at %s)", name, LogHelper.getCallOrigin());
|
||||
}
|
||||
return resId;
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/* Called from SettingsPatch smali */
|
||||
public static int getStringId(String name) {
|
||||
return getIdentifier(name, "string");
|
||||
}
|
||||
|
||||
/* Called from SettingsPatch smali */
|
||||
public static int getDrawableId(String name) {
|
||||
return getIdentifier(name, "drawable");
|
||||
}
|
||||
|
||||
public static String getString(String name) {
|
||||
return ifContextAttached((c) -> c.getString(getStringId(name)), "");
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package tv.twitch.android.app.core;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
public class LandingActivity extends Activity {}
|
@ -0,0 +1,14 @@
|
||||
package tv.twitch.android.feature.settings.menu;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// Dummy
|
||||
public final class SettingsMenuGroup {
|
||||
public SettingsMenuGroup(List<Object> settingsMenuItems) {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
|
||||
public List<Object> getSettingsMenuItems() {
|
||||
throw new UnsupportedOperationException("Stub");
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package tv.twitch.android.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
public class SettingsActivity extends Activity {}
|
@ -0,0 +1,9 @@
|
||||
package tv.twitch.android.shared.chat.util;
|
||||
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
|
||||
public final class ClickableUsernameSpan extends ClickableSpan {
|
||||
@Override
|
||||
public void onClick(View widget) {}
|
||||
}
|
Loading…
Reference in New Issue
Block a user