package tk.wasdennnoch.androidn_ify.systemui.qs;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.View;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import tk.wasdennnoch.androidn_ify.XposedHook;
import tk.wasdennnoch.androidn_ify.systemui.notifications.StatusBarHeaderHooks;
import tk.wasdennnoch.androidn_ify.systemui.qs.tiles.NekoTile;
import tk.wasdennnoch.androidn_ify.utils.ConfigUtils;
import tk.wasdennnoch.androidn_ify.utils.MiscUtils;
import tk.wasdennnoch.androidn_ify.utils.SettingsUtils;
import tk.wasdennnoch.androidn_ify.utils.ViewUtils;
@SuppressLint("StaticFieldLeak")
public class QSTileHostHooks {
public static final String TAG = "QSTileHostHooks";
static final String CLASS_TILE_HOST = "com.android.systemui.statusbar.phone.QSTileHost";
private static final String CLASS_CUSTOM_HOST = "com.android.systemui.tuner.QsTuner$CustomHost";
private static final String CLASS_QS_UTILS_M = "org.cyanogenmod.internal.util.QSUtils";
private static final String CLASS_QS_CONSTANTS_M = "org.cyanogenmod.internal.util.QSConstants";
private static final String CLASS_QS_UTILS_L = "com.android.internal.util.cm.QSUtils";
private static final String CLASS_QS_CONSTANTS_L = "com.android.internal.util.cm.QSConstants";
private static final String CLASS_TUNER_SERVICE = "com.android.systemui.tuner.TunerService";
private static final String TILES_SETTING = "sysui_qs_tiles";
private static final String TILES_SECURE = "sysui_qs_tiles_secure";
static final String TILE_SPEC_NAME = "tileSpec";
public static final String KEY_QUICKQS_TILEVIEW = "QuickQS_TileView";
static final String KEY_EDIT_TILEVIEW = "Edit_TileView";
private static TilesManager mTilesManager = null;
static List<String> mTileSpecs = null;
static List<String> mSecureTiles = new ArrayList<>();
private static Class<?> classQSUtils;
private static Class<?> classQSConstants;
private static Class<?> classCustomHost;
private static Object mTileHost = null;
public static KeyguardMonitor mKeyguard;
private static boolean mIsCm;
private static final List<String> GB_TILE_KEYS = new ArrayList<>(Arrays.asList(
"gb_tile_battery",
"gb_tile_nfc",
"gb_tile_gps_slimkat",
"gb_tile_gps_alt",
"gb_tile_ringer_mode",
"gb_tile_volume",
"gb_tile_network_mode",
"gb_tile_smart_radio",
"gb_tile_sync",
"gb_tile_torch",
"gb_tile_sleep",
"gb_tile_stay_awake",
"gb_tile_quickrecord",
"gb_tile_quickapp",
"gb_tile_quickapp2",
"gb_tile_expanded_desktop",
"gb_tile_screenshot",
"gb_tile_gravitybox",
"gb_tile_usb_tether",
"gb_tile_music",
"gb_tile_lock_screen",
"gb_tile_quiet_hours",
"gb_tile_compass"
));
// MM
private static final XC_MethodHook onTuningChangedHook = new XC_MethodHook(XC_MethodHook.PRIORITY_HIGHEST) {
@SuppressWarnings("unchecked")
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if (classCustomHost != null && classCustomHost.isAssignableFrom(param.thisObject.getClass()))
return;
XposedHook.logD(TAG, "onTuningChangedHook#before called with key '" + param.args[0] + "' and newValue '" + param.args[1] + "'");
if (mTileHost == null)
mTileHost = param.thisObject;
if (mKeyguard == null)
mKeyguard = new KeyguardMonitor((Context) XposedHelpers.getObjectField(param.thisObject, "mContext"), XposedHelpers.getObjectField(param.thisObject, "mKeyguard"));
if (mTilesManager == null)
mTilesManager = new TilesManager(mTileHost);
String newValue = (String) param.args[1];
if (TILES_SECURE.equals(param.args[0])) {
mSecureTiles = loadSecureTilesFromList(newValue);
mTilesManager.onSecureTilesChanged(mSecureTiles);
param.setResult(null);
}
}
@SuppressWarnings("unchecked")
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if (!TILES_SETTING.equals(param.args[0])) return;
Map<String, Object> mTiles = (Map<String, Object>) XposedHelpers.getObjectField(param.thisObject, "mTiles");
List<String> mTileSpecs;
try {
mTileSpecs = (List<String>) XposedHelpers.getObjectField(param.thisObject, "mTileSpecs");
} catch (Throwable t) { // PA
Object tileSpecsWrapper = XposedHelpers.callMethod(param.thisObject, "loadTileSpecs");
mTileSpecs = (List<String>) XposedHelpers.getObjectField(tileSpecsWrapper, "list");
}
final LinkedHashMap<String, Object> newTiles = new LinkedHashMap<>();
for (String tileSpec : mTileSpecs) {
if (mTiles.containsKey(tileSpec)) {
Object tile = mTiles.get(tileSpec);
XposedHook.logD(TAG, "Using available tile for spec " + tileSpec + " with class " + tile.getClass().getName());
XposedHelpers.setAdditionalInstanceField(tile, TILE_SPEC_NAME, tileSpec);
newTiles.put(tileSpec, tile);
mTiles.remove(tileSpec);
} else { // GB already adds all of its tiles even if they aren't selected so we don't have to create them anymore
XposedHook.logD(TAG, "Creating new tile for spec " + tileSpec);
Object tile = createTile(param.thisObject, tileSpec);
if (tile != null)
newTiles.put(tileSpec, tile);
}
}
for (Map.Entry<String, Object> tile : mTiles.entrySet()) {
if (!mTileSpecs.contains(tile.getKey())) {
Object qsTile = tile.getValue();
// Xposed stores additional keys in a WeakHashMap, so in theory it isn't even necessary to remove them prior to destruction
XposedHelpers.removeAdditionalInstanceField(qsTile, KEY_QUICKQS_TILEVIEW);
XposedHelpers.removeAdditionalInstanceField(qsTile, KEY_EDIT_TILEVIEW);
XposedHelpers.removeAdditionalInstanceField(qsTile, "gbTileKey");
XposedHelpers.callMethod(qsTile, "handleDestroy");
}
}
mTiles.clear();
mTiles.putAll(newTiles);
Object mCallback = XposedHelpers.getObjectField(param.thisObject, "mCallback");
if (mCallback != null)
XposedHelpers.callMethod(mCallback, "onTilesChanged");
}
};
private static XC_MethodReplacement loadTileSpecsHook = new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
if (ConfigUtils.M && param.args != null) {
return loadTileSpecsFromList((String) param.args[0]);
}
else {
return loadTileSpecsFromPrefs((Context) XposedHelpers.callMethod(param.thisObject, "getContext"));
}
}
};
// LP
// TODO GB support for LP
private static final XC_MethodHook recreateTilesHook = new XC_MethodHook(XC_MethodHook.PRIORITY_LOWEST) {
@SuppressWarnings("unchecked")
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if (classCustomHost != null && classCustomHost.isAssignableFrom(param.thisObject.getClass()))
return;
XposedHook.logD(TAG, "recreateTilesHook called");
// Thanks to GravityBox for this
if (mTileHost == null)
mTileHost = param.thisObject;
if (mKeyguard == null)
mKeyguard = new KeyguardMonitor((Context) XposedHelpers.getObjectField(param.thisObject, "mContext"), XposedHelpers.getObjectField(param.thisObject, "mKeyguard"));
mTileSpecs = new ArrayList<>(); // Do this since mTileSpecs doesn't exist on LP
Map<String, Object> tileMap = (Map<String, Object>) XposedHelpers.getObjectField(param.thisObject, "mTiles");
for (Entry<String, Object> entry : tileMap.entrySet()) {
XposedHelpers.callMethod(entry.getValue(), "handleDestroy");
}
tileMap.clear();
mTileSpecs.clear();
}
@SuppressWarnings("unchecked")
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if (classCustomHost != null && classCustomHost.isAssignableFrom(param.thisObject.getClass()))
return;
if (mTilesManager == null)
mTilesManager = new TilesManager(param.thisObject);
List<String> tileSpecs = new ArrayList<>(); // Do this since mTileSpecs doesn't exist on LP
Map<String, Object> tileMap = (Map<String, Object>) XposedHelpers.getObjectField(param.thisObject, "mTiles");
Context context = (Context) XposedHelpers.callMethod(param.thisObject, "getContext");
tileSpecs.clear();
tileSpecs.addAll(loadTileSpecsFromPrefs(context));
tileMap.clear();
if (tileSpecs.size() > 0 && tileSpecs.get(0).equals("default")) {
tileSpecs.remove(0);
tileSpecs.addAll(TileAdapter.getDefaultTiles());
}
int tileSpecCount = tileSpecs.size();
for (int i = 0; i < tileSpecCount; i++) {
String spec = tileSpecs.get(i);
Object tile = createTile(param.thisObject, spec);
if (tile != null)
tileMap.put(spec, tile);
}
Object mCallback = XposedHelpers.getObjectField(param.thisObject, "mCallback");
if (mCallback != null) XposedHelpers.callMethod(mCallback, "onTilesChanged");
}
};
private static List<String> loadTileSpecsFromList(String tileList) {
if (tileList == null || tileList.isEmpty()) {
tileList = TextUtils.join(",", TileAdapter.getDefaultTiles());
XposedHook.logD(TAG, "loadTileSpecsFromList: Using default tiles because tileList is empty");
}
final ArrayList<String> tiles = new ArrayList<>();
boolean addedDefault = false;
for (String tile : tileList.split(",")) {
tile = tile.trim();
if (tile.isEmpty()) continue;
if (tile.equals("default")) {
if (!addedDefault) {
tiles.addAll(TileAdapter.getDefaultTiles());
XposedHook.logD(TAG, "loadTileSpecsFromList: Adding default tiles");
addedDefault = true;
}
} else {
tiles.add(tile);
}
}
if (ConfigUtils.qs().hide_edit_tiles)
tiles.remove("edit");
XposedHook.logD(TAG, "loadTileSpecsFromList: Loaded specs '" + TextUtils.join(", ", tiles) + "'");
mTileSpecs = tiles;
return tiles;
}
private static List<String> loadTileSpecsFromPrefs(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
List<String> tiles = new ArrayList<>();
try {
String jsonString = prefs.getString("qs_tiles", getDefaultTilesPref());
JSONArray jsonArray = new JSONArray(jsonString);
int appCount = jsonArray.length();
for (int i = 0; i < appCount; i++) {
String spec = jsonArray.getString(i);
tiles.add(spec);
}
} catch (JSONException e) {
XposedHook.logE(TAG, "Error loading tile specs from prefs", e);
}
mTileSpecs = tiles;
return tiles;
}
private static List<String> loadSecureTilesFromList(String secureTileList) {
final ArrayList<String> specs = new ArrayList<>();
if (secureTileList == null) {
mSecureTiles = specs;
return specs;
}
for (String tile : secureTileList.split(",")) {
tile = tile.trim();
if (tile.isEmpty()) continue;
specs.add(tile);
}
mSecureTiles = specs;
return specs;
}
@Nullable
private static Object createTile(Object tileHost, String tileSpec) {
try {
Object tile;
if (mTilesManager.getCustomTileSpecs().contains(tileSpec)) {
tile = mTilesManager.createTile(tileSpec).getTile();
} else {
tile = mTilesManager.createAospTile(tileHost, tileSpec).getTile();
}
if (tile == null) return null;
XposedHelpers.setAdditionalInstanceField(tile, TILE_SPEC_NAME, tileSpec);
return tile;
} catch (Throwable t) {
XposedHook.logE(TAG, "Couldn't create tile with spec \"" + tileSpec + "\"", t);
}
return null;
}
public static void hook(ClassLoader classLoader) {
try {
Class<?> classTileHost = XposedHelpers.findClass(CLASS_TILE_HOST, classLoader);
if (ConfigUtils.qs().enable_qs_editor) {
mIsCm = false;
try {
if (ConfigUtils.M) {
classQSUtils = XposedHelpers.findClass(CLASS_QS_UTILS_M, classLoader);
classQSConstants = XposedHelpers.findClass(CLASS_QS_CONSTANTS_M, classLoader);
} else {
classQSUtils = XposedHelpers.findClass(CLASS_QS_UTILS_L, classLoader);
classQSConstants = XposedHelpers.findClass(CLASS_QS_CONSTANTS_L, classLoader);
}
XposedHelpers.findAndHookMethod(classTileHost, "setEditing", boolean.class, new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
if ((boolean) param.args[0]) {
View settingsButton = StatusBarHeaderHooks.getSettingsButton();
int[] loc = new int[2];
ViewUtils.getRelativePosition(loc, settingsButton, StatusBarHeaderHooks.mStatusBarHeaderView);
StatusBarHeaderHooks.showEditDismissingKeyguard(loc[0] + settingsButton.getWidth() / 2,
loc[1] + settingsButton.getHeight() / 2);
}
return null;
}
});
mIsCm = true;
} catch (Throwable ignore) {
}
if (ConfigUtils.M) {
final Class<?> classTunerService = XposedHelpers.findClass(CLASS_TUNER_SERVICE, classLoader);
XposedBridge.hookAllConstructors(classTileHost, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Object tunerService = XposedHelpers.callStaticMethod(classTunerService, "get", XposedHelpers.getObjectField(param.thisObject, "mContext"));
if (mIsCm) // CM QSTileHost doesn't use Settings.Secure, so we need to add it.
XposedHelpers.callMethod(tunerService, "addTunable", param.thisObject, TILES_SETTING);
try {
XposedHelpers.callMethod(tunerService, "addTunable", param.thisObject, TILES_SECURE);
} catch (Throwable t) { // Candy6
// The Candy QSTileHost is copied from LP, so just ignore this and let the LP code do the magic
}
}
});
}
try {
classCustomHost = XposedHelpers.findClass(CLASS_CUSTOM_HOST, classLoader);
} catch (Throwable ignore) {
}
if (!ConfigUtils.M) {
XposedHelpers.findAndHookMethod(classTileHost, "recreateTiles", recreateTilesHook);
} else {
try {
XposedHelpers.findAndHookMethod(classTileHost, "onTuningChanged", String.class, String.class, onTuningChangedHook);
} catch (Throwable t) { // Candy6
try {
XposedHelpers.findAndHookMethod(classTileHost, "recreateTiles", recreateTilesHook);
} catch (Throwable t2) {
XposedHook.logE(TAG, "Couldn't hook recreateTiles / onTuningChanged", null);
}
}
}
try {
XposedHelpers.findAndHookMethod(classTileHost, "loadTileSpecs", String.class, loadTileSpecsHook);
} catch (Throwable t) { // OOS3
XposedHelpers.findAndHookMethod(classTileHost, "loadTileSpecs", loadTileSpecsHook);
}
XposedHelpers.findAndHookMethod(classTileHost, "createTile", String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if (classCustomHost != null && classCustomHost.isAssignableFrom(param.thisObject.getClass()))
return;
if (mTilesManager == null)
mTilesManager = new TilesManager(param.thisObject);
String tileSpec = (String) param.args[0];
if (TilesManager.mCustomTileSpecs.contains(tileSpec)) {
param.setResult(mTilesManager.createTile(tileSpec).getTile());
}
}
});
}
} catch (Throwable t) {
XposedHook.logE(TAG, "Error in hook", t);
}
}
@SuppressLint("CommitPrefEdits")
static void saveTileSpecs(Context context, List<String> specs) {
if (ConfigUtils.M) {
SettingsUtils.putStringForCurrentUser(context.getContentResolver(), TILES_SETTING, TextUtils.join(",", specs));
} else {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putString("qs_tiles", new JSONArray(specs).toString());
editor.commit();
}
}
// TODO secure tiles on LP
@SuppressLint("CommitPrefEdits")
static void saveSecureTileSpecs(Context context, List<String> specs) {
String s = "";
for (String sp : specs) s += sp;
XposedHook.logD(TAG, "saveSecureTileSpecs called with specs: " + s);
if (ConfigUtils.M) {
SettingsUtils.putStringForCurrentUser(context.getContentResolver(), TILES_SECURE, TextUtils.join(",", specs));
} else {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putString("qs_tiles_secure", new JSONArray(specs).toString());
editor.commit();
}
}
private static String getDefaultTilesPref() {
return new JSONArray(TileAdapter.getDefaultTiles()).toString();
}
@SuppressWarnings("unchecked")
static List<String> getAvailableTiles(Context context) {
List<String> specs = new ArrayList<>();
try { // Get the available tiles from the SystemUI config.xml
String[] availableSpecs = context.getString(
context.getResources().getIdentifier("quick_settings_tiles_default", "string", XposedHook.PACKAGE_SYSTEMUI))
.split(",");
if (ConfigUtils.qs().alternative_qs_loading || (availableSpecs.length == 1 && availableSpecs[0].equals("default")))
throw new Throwable();
for (String s : availableSpecs) {
if (!TextUtils.isEmpty(s))
specs.add(s);
}
XposedHook.logD(TAG, "Found " + specs.size() + " specs in config.xml");
} catch (Throwable t) {
try { // On CM use the QSUtils
try {
specs = (List<String>) XposedHelpers.callStaticMethod(classQSUtils, "getAvailableTiles", context);
} catch (Throwable t2) {
specs = (ArrayList<String>) ((ArrayList<String>) XposedHelpers.getStaticObjectField(classQSConstants, "TILES_AVAILABLE")).clone();
}
XposedHook.logD(TAG, "Found " + specs.size() + " tiles in getAvailableTiles / TILES_AVAILABLE");
} catch (Throwable t2) { // If that fails too try them all
specs.add("wifi");
specs.add("bt");
specs.add("inversion");
specs.add("cell");
specs.add("airplane");
if (ConfigUtils.M)
specs.add("dnd");
specs.add("rotation");
specs.add("flashlight");
specs.add("location");
specs.add("cast");
specs.add("hotspot");
specs.addAll(bruteForceSpecs());
}
}
specs.addAll(TilesManager.mCustomTileSpecs);
specs.remove("edit");
if (!TilesManager.enableNeko)
specs.remove(NekoTile.TILE_SPEC);
if (ConfigUtils.qs().inject_gb_tiles && MiscUtils.isGBInstalled(context)) {
specs.addAll(GB_TILE_KEYS);
}
XposedHook.logD(TAG, "getAvailableTiles: Found " + specs.size() + " specs: " + TextUtils.join(", ", specs));
return specs;
}
private static List<String> getCurrentTileSpecs() {
List<String> specs = new ArrayList<>();
for (String spec : mTileSpecs) {
if (spec == null) return specs;
specs.add(spec);
}
return specs;
}
public static void addSpec(Context context, String spec) {
List<String> specs = getCurrentTileSpecs();
specs.add(spec);
saveTileSpecs(context, specs);
if (!ConfigUtils.M)
recreateTiles();
}
public static void recreateTiles() {
try {
if (!ConfigUtils.M) {
XposedHelpers.callMethod(mTileHost, "recreateTiles");
}
} catch (Throwable t) {
XposedBridge.log(t);
}
}
private static List<String> bruteForceSpecs() {
XposedHook.logD(TAG, "Brute forcing tile specs!");
List<String> specs = new ArrayList<>();
String[] possibleSpecs = new String[]{"dataconnection", "cell1", "cell2", "notifications", "data", "roaming", "dds", "apn", "profiles",
"performance", "adb_network", "nfc", "compass", "lockscreen", "lte", "volume_panel", "screen_timeout", "timeout",
"usb_tether", "heads_up", "ambient_display", "sync", "battery_saver", "caffeine", "music", "next_alarm",
"ime_selector", "ime", "su", "adb", "live_display", "themes", "brightness", "screen_off", "screenoff", "screenshot",
"expanded_desktop", "reboot", "configurations", "navbar", "appcirclebar", "kernel_adiutor", "screenrecord",
"gesture_anywhere", "power_menu", "app_picker", "kill_app", "hw_keys", "sound", "pulse", "pie", "float_mode",
"nightmode", "immersive", "floating", "halo", "stamina", "datatraffic", "screenmirroring", "throw", "volte",
"tethering", "detectusbdevice", "audioprofile", "hotknot"};
for (String s : possibleSpecs) {
if (bruteForceSpec(s)) specs.add(s);
}
XposedHook.logD(TAG, "bruteForceSpecs: found " + specs.size() + " applicable tile specs");
return specs;
}
private static boolean bruteForceSpec(String spec) {
try {
XposedHelpers.callMethod(mTileHost, "createTile", spec);
return true;
} catch (Throwable ignore) {
return false;
// Not an applicable tile spec
}
}
}