├── app
├── .gitignore
├── src
│ └── main
│ │ ├── assets
│ │ └── xposed_init
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ └── xml
│ │ │ └── preferences.xml
│ │ ├── java
│ │ └── com
│ │ │ └── leagueofnewbs
│ │ │ └── glitchify
│ │ │ ├── JSONResponse.java
│ │ │ ├── ColorHelper.java
│ │ │ ├── Color.java
│ │ │ ├── MainSettingsActivity.java
│ │ │ ├── Preferences.java
│ │ │ └── Main.java
│ │ └── AndroidManifest.xml
├── build.gradle
└── proguard-rules.pro
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── README.md
├── LICENSE
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | com.leagueofnewbs.glitchify.Main
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BatedUrGonnaDie/glitchify/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BatedUrGonnaDie/glitchify/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BatedUrGonnaDie/glitchify/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BatedUrGonnaDie/glitchify/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BatedUrGonnaDie/glitchify/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BatedUrGonnaDie/glitchify/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | *.log
9 | /projectFilesBackup
10 | /app/release/*
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #00000000
4 | #00ffffff
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 01 14:09:18 PST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 29
5 | buildToolsVersion '29.0.2'
6 |
7 | defaultConfig {
8 | applicationId "com.leagueofnewbs.glitchify"
9 | minSdkVersion 16
10 | targetSdkVersion 28
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | }
16 | }
17 | }
18 |
19 | dependencies {
20 | compileOnly 'de.robv.android.xposed:api:82'
21 | compileOnly 'de.robv.android.xposed:api:82:sources'
22 | api 'com.android.support:appcompat-v7:28.0.0'
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | android.useNewApkCreator=true
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Glitchify
2 |
3 | This module provides various tweaks to the official twitch.tv app on android using xposed.
4 |
5 | Features:
6 | - Show FFZ emotes directly in chat
7 | - Show FFZ mod badges
8 | - Show BTTV emotes directly in chat
9 | - FFZ and BTTV badges
10 | - Badge hiding
11 | - Combine all bits in one message
12 | - Prevent messages from being removed
13 | - Prevent mods from clearing chat
14 | - Add timestamps to messages
15 | - Adjust username colors to be more readable
16 |
17 | ## Development
18 |
19 | This project uses Android Studio for the IDE. The example to get started will use that.
20 |
21 | 1. Clone the repo `git clone https://github.com/batedurgonnadie/glitchify`.
22 | 2. Open the project with Android Studio.
23 | 3. Start making a patch or feature.
24 | 4. Submit a PR with said patch or feature.
25 |
26 | ### Proguard only PR's
27 | When submitting patches for *only* changes to the proguard class names, please increment
28 | `versionCode` and `versionName`'s patch value in `app/src/main/AndroidManifest.xml`.
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\Alex\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | -keepattributes EnclosingMethod
20 | -keepattributes Signature
21 | -keepattributes Exceptions
22 | -keepattributes *Annotation*
23 |
24 | -keep class com.leagueofnewbs.glitchify.Main{*;}
25 | -keep class com.leagueofnewbs.glitchify.MainSettingsActivity{*;}
26 | -keep class de.robv.android.xposed.**{*;}
--------------------------------------------------------------------------------
/app/src/main/java/com/leagueofnewbs/glitchify/JSONResponse.java:
--------------------------------------------------------------------------------
1 | package com.leagueofnewbs.glitchify;
2 |
3 | import org.json.JSONArray;
4 | import org.json.JSONException;
5 | import org.json.JSONObject;
6 |
7 | class JSONResponse {
8 | private int statusCode;
9 | private String json;
10 |
11 | JSONResponse(int statusCode, String json) {
12 | this.statusCode = statusCode;
13 | this.json = json;
14 | }
15 |
16 | int getStatusCode() {
17 | return statusCode;
18 | }
19 |
20 | JSONObject jsonAsObject() throws Exception {
21 | JSONObject object;
22 | try {
23 | object = new JSONObject(this.json);
24 | } catch (JSONException e) {
25 | object = new JSONObject();
26 | }
27 | if (object.isNull("status")) {
28 | object.put("status", "" + this.statusCode);
29 | }
30 | return object;
31 | }
32 |
33 | JSONArray jsonAsArray() {
34 | try {
35 | return new JSONArray(json);
36 | } catch (JSONException e) {
37 | return new JSONArray();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 BatedUrGonnaDie
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
15 |
18 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/leagueofnewbs/glitchify/ColorHelper.java:
--------------------------------------------------------------------------------
1 | package com.leagueofnewbs.glitchify;
2 |
3 | import java.util.concurrent.ConcurrentHashMap;
4 |
5 | class ColorHelper {
6 |
7 | private static final ColorHelper INSTANCE = new ColorHelper();
8 | private final ConcurrentHashMap cache = new ConcurrentHashMap<>();
9 |
10 | private ColorHelper() {}
11 |
12 | static ColorHelper getInstance() {
13 | return INSTANCE;
14 | }
15 |
16 | // All brighten code taken from FFZ, and adapted for java/android
17 | // https://github.com/FrankerFaceZ/FrankerFaceZ
18 | Integer maybeBrighten(int color, boolean dark) {
19 | Integer cachedColor = cache.get(color);
20 | if (cachedColor != null) {
21 | return cachedColor;
22 | }
23 | Color outputColor = new Color(color);
24 | int i = 0;
25 | if (dark) {
26 | while (outputColor.luminance() < 0.15 && i++ < 127) {
27 | outputColor.brighten(1);
28 | }
29 | } else {
30 | while (outputColor.luminance() > 0.3 && i++ < 127) {
31 | outputColor.brighten(-1);
32 | }
33 | }
34 |
35 | cache.put(color, outputColor.toInt());
36 | return outputColor.toInt();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/leagueofnewbs/glitchify/Color.java:
--------------------------------------------------------------------------------
1 | package com.leagueofnewbs.glitchify;
2 |
3 | class Color {
4 |
5 | private int r;
6 | private int g;
7 | private int b;
8 | private final int a;
9 |
10 | Color(int color) {
11 | b = color & 255;
12 | g = (color >> 8) & 255;
13 | r = (color >> 16) & 255;
14 | a = (color >> 24) & 255;
15 | }
16 |
17 | Integer toInt() {
18 | int rgba = a;
19 | rgba = (rgba << 8) + r;
20 | rgba = (rgba << 8) + g;
21 | rgba = (rgba << 8) + b;
22 | return rgba;
23 | }
24 |
25 | void brighten(double amount) {
26 | amount = Math.round(255 * (amount / 100));
27 |
28 | r = Math.max(0, Math.min(255, r + (int) amount));
29 | g = Math.max(0, Math.min(255, g + (int) amount));
30 | b = Math.max(0, Math.min(255, b + (int) amount));
31 | }
32 |
33 | double luminance() {
34 | double red = bit2linear(r / 255d);
35 | double green = bit2linear(g / 255d);
36 | double blue = bit2linear(b / 255d);
37 |
38 | return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
39 | }
40 |
41 | private double bit2linear(double channel) {
42 | return (channel <= 0.04045) ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/leagueofnewbs/glitchify/MainSettingsActivity.java:
--------------------------------------------------------------------------------
1 | package com.leagueofnewbs.glitchify;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.Build;
5 | import android.os.Bundle;
6 | import android.preference.PreferenceFragment;
7 | import android.preference.PreferenceManager;
8 | import android.support.v7.app.AppCompatActivity;
9 |
10 | import java.io.File;
11 |
12 | import de.robv.android.xposed.XposedBridge;
13 |
14 | public class MainSettingsActivity extends AppCompatActivity {
15 | @Override
16 | protected void onCreate(Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | getFragmentManager().beginTransaction().replace(android.R.id.content, new MainSettingsFragment()).commit();
19 | }
20 |
21 | public static class MainSettingsFragment extends PreferenceFragment {
22 | @Override
23 | public void onCreate(final Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | PreferenceManager pref = getPreferenceManager();
26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
27 | pref.setStorageDeviceProtected();
28 | }
29 | pref.setSharedPreferencesName("preferences");
30 | addPreferencesFromResource(R.xml.preferences);
31 | }
32 |
33 | @SuppressLint("SetWorldReadable")
34 | @Override
35 | public void onPause() {
36 | super.onPause();
37 | android.content.pm.ApplicationInfo info = getActivity().getApplicationInfo();
38 | String dataPath;
39 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
40 | dataPath = info.deviceProtectedDataDir;
41 | } else {
42 | dataPath = info.dataDir;
43 | }
44 | File dataDir = new File(dataPath);
45 | boolean setReadResult = dataDir.setReadable(true, false);
46 | boolean setExecResult = dataDir.setExecutable(true, false);
47 | if (!setReadResult && !setExecResult)
48 | XposedBridge.log("LoN: Cannot set permissions for preferences!");
49 |
50 | File prefsDir = new File(dataPath, "shared_prefs");
51 | setReadResult = prefsDir.setReadable(true, false);
52 | setExecResult = prefsDir.setExecutable(true, false);
53 | if (!setReadResult && !setExecResult)
54 | XposedBridge.log("LoN: Cannot set permissions for preferences!");
55 |
56 | PreferenceManager pref = getPreferenceManager();
57 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
58 | pref.setStorageDeviceProtected();
59 | }
60 | File prefsFile = new File(prefsDir, pref.getSharedPreferencesName() + ".xml");
61 | if (prefsFile.exists()) {
62 | boolean result = prefsFile.setReadable(true, false);
63 |
64 | if (!result) {
65 | XposedBridge.log("LoN: Could not set preferences as readable, settings will not be loaded!");
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/leagueofnewbs/glitchify/Preferences.java:
--------------------------------------------------------------------------------
1 | package com.leagueofnewbs.glitchify;
2 |
3 | import java.util.ArrayList;
4 | import java.util.concurrent.ConcurrentHashMap;
5 |
6 | import de.robv.android.xposed.XSharedPreferences;
7 |
8 | class Preferences {
9 | private final ConcurrentHashMap pref = new ConcurrentHashMap<>();
10 |
11 | Preferences(XSharedPreferences prefs) {
12 | pref.put("ffzEmotes", prefs.getBoolean("ffz_emotes_enable", true));
13 | pref.put("ffzBadges", prefs.getBoolean("ffz_badges_enable", true));
14 | pref.put("ffzModBadge", prefs.getBoolean("ffz_mod_enable", true));
15 | pref.put("ffzModBadgeURL", "");
16 | pref.put("ffzModeBadgeScale", 1);
17 | pref.put("bttvEmotes", prefs.getBoolean("bttv_emotes_enable", true));
18 | pref.put("bttvBadges", prefs.getBoolean("bttv_badges_enable", true));
19 | pref.put("disableGifEmotes", prefs.getBoolean("disable_gif", false));
20 | pref.put("bitsCombine", prefs.getBoolean("bits_combine_enable", true));
21 | pref.put("hiddenBadges", new ArrayList());
22 | String hiddenBadges = prefs.getString("badge_hiding_enable", "");
23 | if (!hiddenBadges.equals("")) {
24 | for (String key : hiddenBadges.split(",")) {
25 | //noinspection unchecked
26 | ((ArrayList)pref.get("hiddenBadges")).add(key.trim());
27 | }
28 | }
29 | pref.put("preventChatClear", prefs.getBoolean("prevent_channel_clear", true));
30 | pref.put("showDeletedMessages", prefs.getBoolean("show_deleted_messages", true));
31 | pref.put("showTimeStamps", prefs.getBoolean("show_timestamps", true));
32 | pref.put("chatScrollbackLength", Integer.valueOf(prefs.getString("chat_scrollback_length", "100")));
33 | pref.put("colorAdjust", prefs.getBoolean("color_adjust", false));
34 | pref.put("hide_video_ads", prefs.getBoolean("hide_video_ads", false));
35 |
36 | XSharedPreferences darkPrefs = new XSharedPreferences("tv.twitch.android.app", "tv.twitch.android.app_preferences");
37 | pref.put("isDark", darkPrefs.getBoolean("dark_theme_enabled", false));
38 | }
39 |
40 | boolean ffzEmotes() {
41 | return (boolean) pref.get("ffzEmotes");
42 | }
43 |
44 | boolean ffzBadges() {
45 | return (boolean) pref.get("ffzBadges");
46 | }
47 |
48 | boolean ffzModBadge() {
49 | return (boolean) pref.get("ffzModBadge");
50 | }
51 |
52 | String ffzModBadgeURL() {
53 | return (String) pref.get("ffzModBadgeURL");
54 | }
55 |
56 | Integer ffzModBadgeScale() {
57 | return (Integer) pref.get("ffzModBadgeScale");
58 | }
59 |
60 | void ffzModBadgeScale(Integer scale) {
61 | pref.put("ffzModBadgeScale", scale);
62 | }
63 |
64 | void ffzModBadgeURL(String url) {
65 | pref.put("ffzModBadgeURL", url);
66 | }
67 |
68 | boolean bttvEmotes() {
69 | return (boolean) pref.get("bttvEmotes");
70 | }
71 |
72 | boolean bttvBadges() {
73 | return (boolean) pref.get("bttvBadges");
74 | }
75 |
76 | boolean disableGifEmotes() {
77 | return (boolean) pref.get("disableGifEmotes");
78 | }
79 |
80 | boolean bitsCombine() {
81 | return (boolean) pref.get("bitsCombine");
82 | }
83 |
84 | ArrayList hiddenBadges() {
85 | //noinspection unchecked
86 | return (ArrayList) pref.get("hiddenBadges");
87 | }
88 |
89 | boolean preventChatClear() {
90 | return (boolean) pref.get("preventChatClear");
91 | }
92 |
93 | boolean showDeletedMessages() {
94 | return (boolean) pref.get("showDeletedMessages");
95 | }
96 |
97 | boolean showTimeStamps() {
98 | return (boolean) pref.get("showTimeStamps");
99 | }
100 |
101 | Integer chatScrollbackLength() {
102 | return (Integer) pref.get("chatScrollbackLength");
103 | }
104 |
105 | boolean colorAdjust() {
106 | return (boolean) pref.get("colorAdjust");
107 | }
108 |
109 | boolean hideAds() {
110 | return (boolean) pref.get("hide_video_ads");
111 | }
112 |
113 | boolean darkMode() {
114 | return (boolean) pref.get("isDark");
115 | }
116 |
117 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Glitchify
3 |
4 | FrankerFaceZ
5 | ffz_emotes_enable
6 | Enable Emotes
7 | Show emotes in chat
8 |
9 | FrankerFaceZ
10 | ffz_badges_enable
11 | Enable Badges
12 | Show badges in chat
13 |
14 | FrankerFaceZ
15 | ffz_mod_enable
16 | Enable Custom Moderator Badges
17 | Show custom mod badge in channels that have one
18 |
19 |
20 | BetterTwitchTV
21 | bttv_emotes_enable
22 | Enable Emotes
23 | Show emotes in chat
24 |
25 | BetterTwitchTV
26 | bttv_badges_enable
27 | Enable Badges
28 | Show badges in chat
29 |
30 | disable_gif
31 | Disable Gif Emotes
32 | Disable all Gif Emotes
33 |
34 | Badges
35 | badge_hiding_enable
36 | Badges to Hide
37 | List of badges separated by commas
38 |
39 | Bits
40 | bits_combine_enable
41 | Combine Bits
42 | Combine and move bits to the end of messages
43 |
44 | Chat Moderation
45 | prevent_channel_clear
46 | Prevent Chat From Being Cleared
47 | Prevents moderators from being able to clear chat
48 |
49 | show_deleted_messages
50 | Stops Messages From Being Removed
51 | Prevents messages from being replaced with message deleted
52 |
53 | Random Video Options
54 | override_video_quality
55 | Override Saved Video Quality
56 | Use value below instead of your last selected quality for new streams
57 |
58 | Random Options
59 | show_timestamps
60 | Show timestamps in chat
61 | Adds a timestamp of when the message was received
62 |
63 | chat_scrollback_length
64 | Length of Chat History
65 | How far back you can scroll up chat
66 |
67 | color_adjust
68 | Adjust colors to be more readable
69 | Affects user colors in chat
70 |
71 | hide_video_ads
72 | Experimental: Hide video player ads
73 | Prevent ads from being loaded while watching vods
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
15 |
20 |
21 |
22 |
23 |
28 |
33 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
57 |
62 |
67 |
68 |
69 |
70 |
75 |
81 |
86 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/leagueofnewbs/glitchify/Main.java:
--------------------------------------------------------------------------------
1 | package com.leagueofnewbs.glitchify;
2 |
3 | import android.os.Build;
4 | import android.text.Spannable;
5 | import android.text.SpannableString;
6 | import android.text.SpannableStringBuilder;
7 | import android.text.Spanned;
8 | import android.text.SpannedString;
9 | import android.text.style.RelativeSizeSpan;
10 | import android.text.style.StrikethroughSpan;
11 | import android.util.Log;
12 |
13 | import static de.robv.android.xposed.XposedHelpers.*;
14 | import de.robv.android.xposed.IXposedHookLoadPackage;
15 | import de.robv.android.xposed.IXposedHookZygoteInit;
16 | import de.robv.android.xposed.XC_MethodHook;
17 | import de.robv.android.xposed.XSharedPreferences;
18 | import de.robv.android.xposed.XposedBridge;
19 | import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
20 |
21 | import org.json.JSONArray;
22 | import org.json.JSONException;
23 | import org.json.JSONObject;
24 |
25 | import java.io.BufferedReader;
26 | import java.io.File;
27 | import java.io.InputStream;
28 | import java.io.InputStreamReader;
29 | import java.net.URL;
30 | import java.text.SimpleDateFormat;
31 | import java.util.ArrayList;
32 | import java.util.Date;
33 | import java.util.Hashtable;
34 | import java.util.Locale;
35 |
36 | import javax.net.ssl.HttpsURLConnection;
37 |
38 | public class Main implements IXposedHookLoadPackage, IXposedHookZygoteInit {
39 |
40 | private static XSharedPreferences pref;
41 | private Preferences preferences;
42 | private final ColorHelper colorHelper = ColorHelper.getInstance();
43 | private final Hashtable ffzRoomEmotes = new Hashtable<>();
44 | private final Hashtable ffzGlobalEmotes = new Hashtable<>();
45 | private final Hashtable> ffzBadges = new Hashtable<>();
46 | private final Hashtable bttvRoomEmotes = new Hashtable<>();
47 | private final Hashtable bttvGlobalEmotes = new Hashtable<>();
48 | private final Hashtable> bttvBadges = new Hashtable<>();
49 | private static Object customModBadgeImage;
50 | private static final String ffzAPIURL = "https://api.frankerfacez.com/v1/";
51 | private static final String bttvAPIURL = "https://api.betterttv.net/3/cached/";
52 | private static final String bttvUrlTemplate = "https://cdn.betterttv.net/emote/{{id}}/{{image}}";
53 | private static final String logTag = "Glitchify";
54 |
55 | public void initZygote(IXposedHookZygoteInit.StartupParam startupParam) {
56 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
57 | pref = new XSharedPreferences(new File("/data/user_de/0/com.leagueofnewbs.glitchify/shared_prefs/preferences.xml"));
58 | } else {
59 | //noinspection ConstantConditions
60 | pref = new XSharedPreferences(Main.class.getPackage().getName(), "preferences");
61 | }
62 | }
63 |
64 | @SuppressWarnings("RedundantThrows")
65 | public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
66 | if (!lpparam.packageName.equals("tv.twitch.android.app") || !lpparam.isFirstApplication) {
67 | return;
68 | }
69 |
70 | preferences = new Preferences(pref);
71 |
72 | // Get all global info that we can all at once
73 | // FFZ/BTTV global emotes, global twitch badges, and FFZ mod badge
74 | Thread globalThread = new Thread(new Runnable() {
75 | @Override
76 | public void run() {
77 | try {
78 | if (preferences.ffzEmotes()) {
79 | getFFZGlobalEmotes();
80 | }
81 | } catch (Exception e) {
82 | printException(e, "Error fetching global FFZ emotes > ");
83 | }
84 | try {
85 | if (preferences.bttvEmotes()) {
86 | getBTTVGlobalEmotes();
87 | }
88 | } catch (Exception e) {
89 | printException(e, "Error fetching global BTTV emotes > ");
90 | }
91 | try {
92 | if (preferences.ffzBadges()) {
93 | getFFZBadges();
94 | }
95 | } catch (Exception e) {
96 | printException(e, "Error fetching global FFZ badges > ");
97 | }
98 | try {
99 | if (preferences.bttvBadges()) {
100 | getBTTVBadges();
101 | }
102 | } catch (Exception e) {
103 | printException(e, "Error fetching global BTTV badges > ");
104 | }
105 | }
106 | });
107 | globalThread.start();
108 |
109 |
110 | // These are all the different class definitions that are needed in the function hooking
111 | final Class> chatControllerClass = findClass("tv.twitch.android.sdk.z", lpparam.classLoader);
112 | final Class> chatUpdaterClass = findClass("tv.twitch.android.sdk.z$f", lpparam.classLoader);
113 | final Class> chatViewPresenterClass = findClass("tv.twitch.a.k.g.n", lpparam.classLoader);
114 | final Class> messageRecyclerItemClass = findClass("tv.twitch.android.adapters.a.b", lpparam.classLoader);
115 | final Class> channelChatAdapterClass = findClass("tv.twitch.a.k.g.n0.a", lpparam.classLoader);
116 | final Class> chatUtilClass = findClass("tv.twitch.a.k.g.r1.g", lpparam.classLoader);
117 | final Class> deletedMessageClickableSpanClass = findClass("tv.twitch.a.k.g.r1.l", lpparam.classLoader);
118 | final Class> systemMessageTypeClass = findClass("tv.twitch.a.k.g.n0.g", lpparam.classLoader);
119 | final Class> chatMessageFactoryClass = findClass("tv.twitch.a.k.g.e1.a", lpparam.classLoader);
120 | final Class> clickableUsernameSpanClass = findClass("tv.twitch.a.k.g.r1.j", lpparam.classLoader);
121 | final Class> iClickableUsernameSpanListenerClass = findClass("tv.twitch.a.k.g.t0.a", lpparam.classLoader);
122 | final Class> twitchUrlSpanClickListenerInterfaceClass = findClass("tv.twitch.a.k.c0.b.s.g", lpparam.classLoader);
123 | final Class> censoredMessageTrackingInfoClass = findClass("tv.twitch.a.k.g.p1.c", lpparam.classLoader);
124 | final Class> webViewSourceEnumClass = findClass("tv.twitch.android.models.webview.WebViewSource", lpparam.classLoader);
125 | final Class> chatMessageInterfaceClass = findClass("tv.twitch.a.k.g.g", lpparam.classLoader);
126 | final Class> chatBadgeImageClass = findClass("tv.twitch.chat.ChatBadgeImage", lpparam.classLoader);
127 | final Class> bitsTokenClass = findClass("tv.twitch.android.models.chat.MessageToken$BitsToken", lpparam.classLoader);
128 | final Class> cheermotesHelperClass = findClass("tv.twitch.a.k.d.a0.h", lpparam.classLoader);
129 | final Class> chommentModelDelegateClass = findClass("tv.twitch.a.k.g.u0.c", lpparam.classLoader);
130 | final Class> EventDispatcherClass = findClass("tv.twitch.android.core.mvp.viewdelegate.EventDispatcher", lpparam.classLoader);
131 | final Class> channelInfoClass = findClass("tv.twitch.android.models.channel.ChannelInfo", lpparam.classLoader);
132 | final Class> streamTypeClass = findClass("tv.twitch.android.models.streams.StreamType", lpparam.classLoader);
133 | //noinspection unchecked
134 | final Class extends Enum> mediaSpanClass = (Class extends Enum>) findClass("tv.twitch.a.k.c0.b.s.d", lpparam.classLoader);
135 | final Class> vodPlayerPresenterClass = findClass("tv.twitch.a.k.v.j0.w", lpparam.classLoader);
136 | final Class> vodModelClass = findClass("tv.twitch.android.models.videos.VodModel", lpparam.classLoader);
137 | final Class> videoAdManagerClass = findClass("tv.twitch.android.player.ads.VideoAdManager", lpparam.classLoader);
138 | // Updated combined bits insertion object field to find bits helper in ChatMessageFactory
139 |
140 | // This is called when a vod chat widget gets a channel name attached to it
141 | // It sets up all the channel specific stuff (bttv/ffz emotes, etc)
142 | findAndHookMethod(vodPlayerPresenterClass, "a", vodModelClass, int.class, String.class, new XC_MethodHook() {
143 | @Override
144 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
145 | final String channelName = (String) callMethod(param.args[0], "getChannelName");
146 | final int channelId = (int) callMethod(param.args[0], "getBroadcasterId");
147 | getRoomEmotes(channelName, channelId);
148 | }
149 | });
150 |
151 | // This is called when a live chat widget gets a channel name attached to it
152 | // It sets up all the channel specific stuff (bttv/ffz emotes, etc)
153 | findAndHookMethod(chatViewPresenterClass, "a", channelInfoClass, String.class, streamTypeClass, new XC_MethodHook() {
154 | @Override
155 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
156 | final String channelName = (String) callMethod(param.args[0], "getName");
157 | final int channelId = (int) callMethod(param.args[0], "getId");
158 | getRoomEmotes(channelName, channelId);
159 | }
160 | });
161 |
162 | // This is what actually goes through and strikes out the messages
163 | // If show deleted is false this will replace with
164 | findAndHookMethod(chatUtilClass, "a", Spanned.class, String.class, deletedMessageClickableSpanClass, new XC_MethodHook() {
165 | @Override
166 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
167 | if (preferences.showDeletedMessages()) {
168 | Spanned messageSpan = (Spanned) param.args[0];
169 | Object[] spans = messageSpan.getSpans(0, messageSpan.length(), clickableUsernameSpanClass);
170 | if ((spans.length == 0 ? 1 : null) != null) {
171 | param.setResult(null);
172 | return;
173 | }
174 |
175 | int spanEnd = messageSpan.getSpanEnd(spans[0]);
176 | int length = 2 + spanEnd;
177 | if (length < messageSpan.length() && messageSpan.subSequence(spanEnd, length).toString().equals(": ")) {
178 | spanEnd = length;
179 | }
180 | SpannableStringBuilder ssb = new SpannableStringBuilder(messageSpan, 0, spanEnd);
181 | SpannableStringBuilder ssb2 = new SpannableStringBuilder(messageSpan, spanEnd, messageSpan.length());
182 | ssb.append(ssb2);
183 | ssb.setSpan(new StrikethroughSpan(), ssb.length() - ssb2.length(), ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
184 | param.setResult(ssb);
185 | }
186 | }
187 | });
188 |
189 | // Add timestamps to the beginning of every message
190 | findAndHookConstructor(messageRecyclerItemClass, "android.content.Context", String.class, int.class, String.class, String.class, int.class, Spanned.class, systemMessageTypeClass, float.class, int.class, float.class, boolean.class, new XC_MethodHook() {
191 | @Override
192 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
193 | if (preferences.showTimeStamps()) {
194 | SimpleDateFormat formatter = new SimpleDateFormat("h:mm ", Locale.US);
195 | SpannableString dateString = SpannableString.valueOf(formatter.format(new Date()));
196 | dateString.setSpan(new RelativeSizeSpan(0.75f), 0, dateString.length() - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
197 | CharSequence messageSpan = (CharSequence) param.args[6];
198 | SpannableStringBuilder message = new SpannableStringBuilder(dateString);
199 | message.append(messageSpan);
200 | param.args[6] = SpannedString.valueOf(message);
201 | }
202 | }
203 | });
204 |
205 | // Override complete chat clears
206 | findAndHookMethod(chatUpdaterClass, "chatChannelMessagesCleared", int.class, int.class, new XC_MethodHook() {
207 | @Override
208 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
209 | if (preferences.preventChatClear()) {
210 | param.setResult(null);
211 | }
212 | }
213 | });
214 | XposedBridge.hookAllMethods(chatUpdaterClass, "chatChannelModNoticeClearChat", new XC_MethodHook() {
215 | @Override
216 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
217 | if (preferences.preventChatClear()) {
218 | param.setResult(null);
219 | }
220 | }
221 | });
222 |
223 | // Prevent overriding of chat history length
224 | findAndHookConstructor(channelChatAdapterClass, int.class, new XC_MethodHook() {
225 | @Override
226 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
227 | param.args[0] = preferences.chatScrollbackLength();
228 | }
229 | });
230 |
231 | // Inject all badges and emotes into the finished message
232 | findAndHookMethod(chatMessageFactoryClass, "a", chatMessageInterfaceClass, boolean.class, boolean.class, boolean.class, int.class, int.class, iClickableUsernameSpanListenerClass, twitchUrlSpanClickListenerInterfaceClass, webViewSourceEnumClass, String.class, boolean.class, censoredMessageTrackingInfoClass, Integer.class, EventDispatcherClass, new XC_MethodHook() {
233 | @Override
234 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
235 | if (preferences.bitsCombine() && !chommentModelDelegateClass.isInstance(param.args[0])) {
236 | setAdditionalInstanceField(param.thisObject, "allowBitInsertion", false);
237 | }
238 | if (preferences.colorAdjust()) {
239 | Integer color = (Integer) param.args[4];
240 | Integer newColor = colorHelper.maybeBrighten(color, preferences.darkMode());
241 | param.args[4] = newColor;
242 | }
243 | }
244 |
245 | @Override
246 | protected void afterHookedMethod(MethodHookParam param) throws Throwable {
247 | SpannableStringBuilder msg = new SpannableStringBuilder((SpannedString) param.getResult());
248 |
249 | if (preferences.ffzBadges()) {
250 | msg = injectBadges(param, mediaSpanClass, msg, ffzBadges);
251 | }
252 | if (preferences.bttvBadges()) {
253 | msg = injectBadges(param, mediaSpanClass, msg, bttvBadges);
254 | }
255 | if (preferences.ffzEmotes()) {
256 | msg = injectEmotes(param, mediaSpanClass, msg, ffzGlobalEmotes);
257 | msg = injectEmotes(param, mediaSpanClass, msg, ffzRoomEmotes);
258 | }
259 | if (preferences.bttvEmotes()) {
260 | msg = injectEmotes(param, mediaSpanClass, msg, bttvGlobalEmotes);
261 | msg = injectEmotes(param, mediaSpanClass, msg, bttvRoomEmotes);
262 | }
263 |
264 | if (preferences.bitsCombine() && !chommentModelDelegateClass.isInstance(param.args[0])) {
265 | setAdditionalInstanceField(param.thisObject, "allowBitInsertion", true);
266 | Object chatMessageInfo = getObjectField(param.args[0], "a");
267 | int numBits = getIntField(chatMessageInfo, "numBitsSent");
268 | if (numBits > 0) {
269 | Object bit = newInstance(bitsTokenClass, "cheer", numBits);
270 | SpannableString bitString = (SpannableString) callMethod(param.thisObject, "a", bit, getObjectField(param.thisObject, "b"));
271 | if (bitString != null) {
272 | msg.append(" ");
273 | msg.append(bitString);
274 | }
275 | }
276 | }
277 | param.setResult(SpannableString.valueOf(msg));
278 | }
279 | });
280 |
281 | // Stop bits from being put into chat by the message factory
282 | findAndHookMethod(chatMessageFactoryClass, "a", bitsTokenClass, cheermotesHelperClass, new XC_MethodHook() {
283 | @Override
284 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
285 | if (!((Boolean) getAdditionalInstanceField(param.thisObject, "allowBitInsertion"))) {
286 | param.setResult(null);
287 | }
288 | }
289 | });
290 |
291 | // Return null for any hidden badges, for some reason this works and I'm not going to complain because it's much easier this way
292 | // If custom mod badge, return a customized ChatBadgeImage instance with our url for mod badge
293 | // Whenever we leave the chat, return to using the default
294 | findAndHookMethod(chatControllerClass, "a", int.class, String.class, String.class, new XC_MethodHook() {
295 | @Override
296 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
297 | String badgeName = (String) param.args[1];
298 | if (preferences.hiddenBadges().contains(badgeName)) {
299 | param.setResult(null);
300 | }
301 | if (preferences.ffzModBadge() && !preferences.ffzModBadgeURL().equals("") && badgeName.equals("moderator")) {
302 | // Set and save a badge image to be reused for all messages
303 | if (customModBadgeImage == null || !getObjectField(customModBadgeImage, "url").equals(preferences.ffzModBadgeURL())) {
304 | customModBadgeImage = newInstance(chatBadgeImageClass);
305 | setObjectField(customModBadgeImage, "url", preferences.ffzModBadgeURL());
306 | setFloatField(customModBadgeImage, "scale", preferences.ffzModBadgeScale());
307 | }
308 | param.setResult(customModBadgeImage);
309 | }
310 | }
311 | });
312 |
313 | XposedBridge.hookAllMethods(videoAdManagerClass, "requestAds", new XC_MethodHook() {
314 | @Override
315 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
316 | if (preferences.hideAds()) {
317 | param.setResult(null);
318 | }
319 | }
320 | });
321 | }
322 |
323 | private SpannableStringBuilder injectBadges(XC_MethodHook.MethodHookParam param, Class mediaSpanClass, SpannableStringBuilder chatMsg, Hashtable customBadges) {
324 | String chatSender = (String) callMethod(param.args[0], "getDisplayName");
325 | int location = chatMsg.toString().indexOf(chatSender);
326 | if (location == -1) { return chatMsg; }
327 |
328 | int badgeCount;
329 | if (location == 0) {
330 | badgeCount = location;
331 | } else {
332 | badgeCount = chatMsg.toString().substring(0, location - 1).split(" ").length;
333 | }
334 |
335 | for (Object key : customBadges.keySet()) {
336 | if (badgeCount >= 3) {
337 | // Already at 3 badges, anymore will clog up chat box
338 | return chatMsg;
339 | }
340 | String keyString = (String) key;
341 | if (preferences.hiddenBadges().contains(keyString)) {
342 | continue;
343 | }
344 | if (!((ArrayList) ((Hashtable) customBadges.get(keyString)).get("users")).contains(chatSender)) {
345 | continue;
346 | }
347 | String url = (String) ((Hashtable) customBadges.get(keyString)).get("image");
348 | SpannableString badgeSpan = (SpannableString) callMethod(param.thisObject, "a", param.thisObject, url, Enum.valueOf(mediaSpanClass, "Badge"), keyString + " ", null, true, 8, null);
349 | chatMsg.insert(location, badgeSpan);
350 | location += badgeSpan.length();
351 | badgeCount++;
352 | }
353 | return chatMsg;
354 | }
355 |
356 | private SpannableStringBuilder injectEmotes(XC_MethodHook.MethodHookParam param, Class mediaSpanClass, SpannableStringBuilder chatMsg, Hashtable customEmoteHash) {
357 | String chatSender = (String) callMethod(param.args[0], "getDisplayName");
358 | for (Object key : customEmoteHash.keySet()) {
359 | String keyString = (String) key;
360 | int location = chatMsg.toString().indexOf(chatSender);
361 | if (location == -1) { return chatMsg; }
362 |
363 | location++;
364 | int keyLength = keyString.length();
365 | while ((location = chatMsg.toString().indexOf(keyString, location)) != -1) {
366 | try {
367 | if (chatMsg.charAt(location - 1) != ' ' || chatMsg.charAt(location + keyLength) != ' ') {
368 | ++location;
369 | continue;
370 | }
371 | } catch(IndexOutOfBoundsException e) {
372 | // End of line reached
373 | }
374 |
375 | String url = customEmoteHash.get(keyString).toString();
376 | SpannableString emoteSpan = (SpannableString) callMethod(param.thisObject, "a", param.thisObject, url, Enum.valueOf(mediaSpanClass, "Emote"), keyString, null, false, 24, null);
377 | chatMsg.replace(location, location + keyLength, emoteSpan);
378 | location += keyString.length();
379 | }
380 | }
381 |
382 | return chatMsg;
383 | }
384 |
385 | private void getRoomEmotes(final String channelName, final int channelId) {
386 | Thread roomThread = new Thread(new Runnable() {
387 | @Override
388 | public void run() {
389 | try {
390 | if (preferences.ffzEmotes()) {
391 | getFFZRoomEmotes(channelName);
392 | }
393 | } catch (Exception e) {
394 | printException(e, "Error fetching FFZ emotes for " + channelName + " > ");
395 | }
396 | try {
397 | if (preferences.bttvEmotes()) {
398 | getBTTVRoomEmotes(channelId);
399 | }
400 | } catch (Exception e) {
401 | printException(e, "Error fetching BTTV emotes for " + channelName + " > ");
402 | }
403 | }
404 | });
405 | roomThread.start();
406 | }
407 |
408 | private void getFFZRoomEmotes(String channel) throws Exception {
409 | ffzRoomEmotes.clear();
410 | URL roomURL = new URL(ffzAPIURL + "room/" + channel);
411 | JSONObject roomEmotes = getJSON(roomURL).jsonAsObject();
412 | try {
413 | int status = roomEmotes.getInt("status");
414 | if (status == 404) {
415 | preferences.ffzModBadgeURL("");
416 | return;
417 | }
418 | } catch (JSONException e) {
419 | // Required to compile
420 | }
421 | int set = roomEmotes.getJSONObject("room").getInt("set");
422 | if (roomEmotes.getJSONObject("room").isNull("moderator_badge")) {
423 | preferences.ffzModBadgeURL("");
424 | } else {
425 | JSONObject modURLs = roomEmotes.getJSONObject("room").getJSONObject("mod_urls");
426 | String url = modURLs.getString("1");
427 | if (modURLs.has("2")) {
428 | url = modURLs.getString("2");
429 | preferences.ffzModBadgeScale(2);
430 | }
431 | preferences.ffzModBadgeURL("https:" + url + "/solid");
432 | }
433 | JSONArray roomEmoteArray = roomEmotes.getJSONObject("sets").getJSONObject(Integer.toString(set)).getJSONArray("emoticons");
434 | for (int i = 0; i < roomEmoteArray.length(); ++i) {
435 | String emoteName = roomEmoteArray.getJSONObject(i).getString("name");
436 | String emoteURL = roomEmoteArray.getJSONObject(i).getJSONObject("urls").getString("1");
437 | ffzRoomEmotes.put(emoteName, "https:" + emoteURL);
438 | }
439 | }
440 |
441 | private void getFFZGlobalEmotes() throws Exception {
442 | URL globalURL = new URL(ffzAPIURL + "set/global");
443 | JSONObject globalEmotes = getJSON(globalURL).jsonAsObject();
444 | JSONArray setsArray = globalEmotes.getJSONArray("default_sets");
445 | for (int i = 0; i < setsArray.length(); ++i) {
446 | int set = setsArray.getInt(i);
447 | JSONArray globalEmotesArray = globalEmotes.getJSONObject("sets").getJSONObject(Integer.toString(set)).getJSONArray("emoticons");
448 | for (int j = 0; j < globalEmotesArray.length(); ++j) {
449 | String emoteName = globalEmotesArray.getJSONObject(j).getString("name");
450 | String emoteURL = globalEmotesArray.getJSONObject(j).getJSONObject("urls").getString("1");
451 | ffzGlobalEmotes.put(emoteName, "https:" + emoteURL);
452 | }
453 | }
454 | }
455 |
456 | @SuppressWarnings({"unchecked", "ConstantConditions"})
457 | private void getFFZBadges() throws Exception {
458 | URL badgeURL = new URL(ffzAPIURL + "badges");
459 | JSONObject badges = getJSON(badgeURL).jsonAsObject();
460 | JSONArray badgesList = badges.getJSONArray("badges");
461 | for (int i = 0; i < badgesList.length(); ++i) {
462 | String name = "ffz-" + badgesList.getJSONObject(i).getString("name");
463 | ffzBadges.put(name, new Hashtable());
464 |
465 | String imageLocation = "https:" + badgesList.getJSONObject(i).getJSONObject("urls").getString("2") + "/solid";
466 | ffzBadges.get(name).put("image", imageLocation);
467 | ffzBadges.get(name).put("users", new ArrayList());
468 |
469 | JSONArray userList = badges.getJSONObject("users").getJSONArray(badgesList.getJSONObject(i).getString("id"));
470 | for (int j = 0; j < userList.length(); ++j) {
471 | ((ArrayList) ffzBadges.get(name).get("users")).add(userList.getString(j).toLowerCase());
472 | }
473 | }
474 | }
475 |
476 | private void getBTTVGlobalEmotes() throws Exception {
477 | URL globalURL = new URL(bttvAPIURL + "emotes/global");
478 | JSONResponse response = getJSON(globalURL);
479 | int status = response.getStatusCode();
480 | if (status != 200) {
481 | XposedBridge.log("LoN: Error fetching bttv global emotes (" + status + ")");
482 | return;
483 | }
484 |
485 | JSONArray globalEmotesArray = response.jsonAsArray();
486 | if (globalEmotesArray.length() == 0) {
487 | XposedBridge.log("LoN: BTTV global emotes came back empty");
488 | return;
489 | }
490 |
491 | for (int i = 0; i < globalEmotesArray.length(); ++i) {
492 | if(!(preferences.disableGifEmotes() && globalEmotesArray.getJSONObject(i).getString("imageType").equals("gif"))) {
493 | String emoteName = globalEmotesArray.getJSONObject(i).getString("code");
494 | String emoteID = globalEmotesArray.getJSONObject(i).getString("id");
495 | String emoteURL = bttvUrlTemplate.replace("{{id}}", emoteID).replace("{{image}}", "1x");
496 | bttvGlobalEmotes.put(emoteName, emoteURL);
497 | }
498 | }
499 | }
500 |
501 | private void getBTTVRoomEmotes(int channelId) throws Exception {
502 | bttvRoomEmotes.clear();
503 | URL roomURL = new URL(bttvAPIURL + "users/twitch/" + channelId);
504 | JSONObject roomEmotes = getJSON(roomURL).jsonAsObject();
505 | int status = roomEmotes.getInt("status");
506 | if (status != 200) {
507 | if (status != 404) {
508 | XposedBridge.log("LoN: Error fetching bttv room emotes (" + status + ")");
509 | }
510 | return;
511 | }
512 |
513 | JSONArray roomEmotesArray = roomEmotes.getJSONArray("channelEmotes");
514 | JSONArray sharedEmotesArray = roomEmotes.getJSONArray("sharedEmotes");
515 | for (int i = 0; i < roomEmotesArray.length(); ++i) {
516 | if(!(preferences.disableGifEmotes() && roomEmotesArray.getJSONObject(i).getString("imageType").equals("gif"))) {
517 | String emoteName = roomEmotesArray.getJSONObject(i).getString("code");
518 | String emoteID = roomEmotesArray.getJSONObject(i).getString("id");
519 | String emoteURL = bttvUrlTemplate.replace("{{id}}", emoteID).replace("{{image}}", "1x");
520 | bttvRoomEmotes.put(emoteName, emoteURL);
521 | }
522 | }
523 | for (int i = 0; i < sharedEmotesArray.length(); ++i) {
524 | if(!(preferences.disableGifEmotes() && sharedEmotesArray.getJSONObject(i).getString("imageType").equals("gif"))) {
525 | String emoteName = sharedEmotesArray.getJSONObject(i).getString("code");
526 | String emoteID = sharedEmotesArray.getJSONObject(i).getString("id");
527 | String emoteURL = bttvUrlTemplate.replace("{{id}}", emoteID).replace("{{image}}", "1x");
528 | bttvRoomEmotes.put(emoteName, emoteURL);
529 | }
530 | }
531 | }
532 |
533 | @SuppressWarnings({"unchecked", "ConstantConditions"})
534 | private void getBTTVBadges() throws Exception {
535 | URL badgeURL = new URL(bttvAPIURL + "badges");
536 | JSONResponse response = getJSON(badgeURL);
537 | if (response.getStatusCode() != 200) {
538 | XposedBridge.log("LoN: Error fetching bttv badges");
539 | return;
540 | }
541 |
542 | JSONArray badges = response.jsonAsArray();
543 | if (badges.length() == 0) {
544 | XposedBridge.log("LoN: BTTV badges came back empty");
545 | return;
546 | }
547 |
548 | Hashtable badgeConversion = new Hashtable();
549 | badgeConversion.put("NightDev Developer", "bttv-developer");
550 | badgeConversion.put("NightDev Support Team", "bttv-support");
551 | badgeConversion.put("NightDev Design Team", "bttv-design");
552 | badgeConversion.put("BetterTTV Emote Approver", "bttv-emotes");
553 | for (int i = 0; i < badges.length(); ++i) {
554 | String name = badgeConversion.get(badges.getJSONObject(i).getJSONObject("badge").getString("description"));
555 | String user = badges.getJSONObject(i).getString("name");
556 | if (bttvBadges.get(name) == null) {
557 | bttvBadges.put(name, new Hashtable());
558 | String imageLocation = "";
559 | switch(name) {
560 | case "bttv-developer": { imageLocation = "https://leagueofnewbs.com/images/bttv-dev.png"; break; }
561 | case "bttv-support": { imageLocation = "https://leagueofnewbs.com/images/bttv-support.png"; break; }
562 | case "bttv-design": { imageLocation = "https://leagueofnewbs.com/images/bttv-design.png"; break; }
563 | case "bttv-emotes": { imageLocation = "https://leagueofnewbs.com/images/bttv-approver.png"; break; }
564 | }
565 | bttvBadges.get(name).put("image", imageLocation);
566 | bttvBadges.get(name).put("users", new ArrayList());
567 | }
568 |
569 | ((ArrayList) bttvBadges.get(name).get("users")).add(user);
570 | }
571 | }
572 |
573 | private JSONResponse getJSON(URL url) throws Exception {
574 | HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
575 | conn.setRequestMethod("GET");
576 | conn.setRequestProperty("User-Agent", "Glitchify|bated@leagueofnewbs.com");
577 | if (url.getHost().contains("twitch.tv")) {
578 | conn.setRequestProperty("Client-ID", "2pvhvz6iubpg0ny77pyb1qrjynupjdu");
579 | }
580 | InputStream inStream;
581 | int responseCode = conn.getResponseCode();
582 | if (responseCode >= 400) {
583 | inStream = conn.getErrorStream();
584 | } else {
585 | inStream = conn.getInputStream();
586 | }
587 | BufferedReader buffReader = new BufferedReader(new InputStreamReader(inStream));
588 | StringBuilder jsonString = new StringBuilder();
589 | String line;
590 | while ((line = buffReader.readLine()) != null) {
591 | jsonString.append(line);
592 | }
593 | buffReader.close();
594 |
595 | return new JSONResponse(responseCode, jsonString.toString());
596 | }
597 |
598 | private void printException(Exception e, String prefix) {
599 | if (e.getMessage() == null || e.getMessage().equals("")) {
600 | return;
601 | }
602 | String output = "LoN: ";
603 | if (prefix != null) {
604 | output += prefix;
605 | }
606 | output += e.getMessage();
607 | XposedBridge.log(output);
608 | Log.e(logTag, output, e);
609 | }
610 |
611 | }
612 |
--------------------------------------------------------------------------------