package de.robv.android.xposed.mods.appsettings; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import static de.robv.android.xposed.XposedHelpers.getAdditionalInstanceField; import static de.robv.android.xposed.XposedHelpers.getObjectField; import static de.robv.android.xposed.XposedHelpers.removeAdditionalInstanceField; import static de.robv.android.xposed.XposedHelpers.setAdditionalInstanceField; import static de.robv.android.xposed.XposedHelpers.setFloatField; import static de.robv.android.xposed.XposedHelpers.setIntField; import static de.robv.android.xposed.XposedHelpers.setObjectField; import java.util.Locale; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.AndroidAppHelper; import android.app.Notification; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.XResources; import android.content.res.XResources.DimensionReplacement; import android.media.AudioTrack; import android.media.JetPlayer; import android.media.MediaPlayer; import android.media.SoundPool; import android.os.Build; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Display; import android.view.Surface; import android.view.SurfaceHolder; import android.view.ViewConfiguration; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodReplacement; import de.robv.android.xposed.XSharedPreferences; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; import de.robv.android.xposed.mods.appsettings.hooks.Activities; import de.robv.android.xposed.mods.appsettings.hooks.PackagePermissions; public class XposedMod implements IXposedHookZygoteInit, IXposedHookLoadPackage { public static final String this_package = XposedMod.class.getPackage().getName(); private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; private static final String[] SYSTEMUI_ADJUSTED_DIMENSIONS = { "status_bar_height", "navigation_bar_height", "navigation_bar_height_landscape", "navigation_bar_width", "system_bar_height" }; public static XSharedPreferences prefs; @Override public void initZygote(IXposedHookZygoteInit.StartupParam startupParam) throws Throwable { loadPrefs(); adjustSystemDimensions(); // Hook to override DPI (globally, including resource load + rendering) try { if (Build.VERSION.SDK_INT < 17) { findAndHookMethod(Display.class, "init", int.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { String packageName = AndroidAppHelper.currentPackageName(); if (!isActive(packageName)) { // No overrides for this package return; } int packageDPI = prefs.getInt(packageName + Common.PREF_DPI, prefs.getInt(Common.PREF_DEFAULT + Common.PREF_DPI, 0)); if (packageDPI > 0) { // Density for this package is overridden, change density setFloatField(param.thisObject, "mDensity", packageDPI / 160.0f); } }; }); } else { findAndHookMethod(Display.class, "updateDisplayInfoLocked", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { String packageName = AndroidAppHelper.currentPackageName(); if (!isActive(packageName)) { // No overrides for this package return; } int packageDPI = prefs.getInt(packageName + Common.PREF_DPI, prefs.getInt(Common.PREF_DEFAULT + Common.PREF_DPI, 0)); if (packageDPI > 0) { // Density for this package is overridden, change density Object mDisplayInfo = getObjectField(param.thisObject, "mDisplayInfo"); setIntField(mDisplayInfo, "logicalDensityDpi", packageDPI); } }; }); } } catch (Throwable t) { XposedBridge.log(t); } // Override settings used when loading resources try { findAndHookMethod(Resources.class, "updateConfiguration", Configuration.class, DisplayMetrics.class, "android.content.res.CompatibilityInfo", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { if (param.args[0] == null) return; /* Starting with XposedBridge v52, by the time updateConfiguration is called this object * might not yet be an XResources instance where the package name can be fetched fom. * If that's the case, use the newly introduced getPackageNameDuringConstruction method * which was introduced for this purpose and fetches the package name for this Resource * in the middle of initialization. */ String packageName; Resources res = ((Resources) param.thisObject); if (res instanceof XResources) { packageName = ((XResources) res).getPackageName(); } else { try { packageName = XResources.getPackageNameDuringConstruction(); } catch (IllegalStateException e) { // That's ok, we might have been called for // non-standard resources return; } } String hostPackageName = AndroidAppHelper.currentPackageName(); boolean isActiveApp = hostPackageName.equals(packageName); // Workaround for KitKat. The keyguard is a different package now but runs in the // same process as SystemUI and displays as main package if (Build.VERSION.SDK_INT >= 19 && hostPackageName.equals("com.android.keyguard")) hostPackageName = SYSTEMUI_PACKAGE; // If setting enabled to also modify resources on other host packages (typically // for widgets), simulate that this package is not hosted by another one // For everything except the process-wide default Locale - see below if (isActive(packageName, Common.PREF_RES_ON_WIDGETS)) hostPackageName = packageName; // settings related to the density etc. are calculated for the running app... Configuration newConfig = null; if (hostPackageName != null && isActive(hostPackageName)) { int screen = prefs.getInt(hostPackageName + Common.PREF_SCREEN, prefs.getInt(Common.PREF_DEFAULT + Common.PREF_SCREEN, 0)); if (screen < 0 || screen >= Common.swdp.length) screen = 0; int dpi = prefs.getInt(hostPackageName + Common.PREF_DPI, prefs.getInt(Common.PREF_DEFAULT + Common.PREF_DPI, 0)); int fontScale = prefs.getInt(hostPackageName + Common.PREF_FONT_SCALE, prefs.getInt(Common.PREF_DEFAULT + Common.PREF_FONT_SCALE, 0)); int swdp = Common.swdp[screen]; int wdp = Common.wdp[screen]; int hdp = Common.hdp[screen]; boolean xlarge = prefs.getBoolean(hostPackageName + Common.PREF_XLARGE, false); if (swdp > 0 || xlarge || dpi > 0 || fontScale > 0) { newConfig = new Configuration((Configuration) param.args[0]); DisplayMetrics newMetrics; if (param.args[1] != null) { newMetrics = new DisplayMetrics(); newMetrics.setTo((DisplayMetrics) param.args[1]); param.args[1] = newMetrics; } else { newMetrics = res.getDisplayMetrics(); } if (swdp > 0) { newConfig.smallestScreenWidthDp = swdp; newConfig.screenWidthDp = wdp; newConfig.screenHeightDp = hdp; } if (xlarge) newConfig.screenLayout |= Configuration.SCREENLAYOUT_SIZE_XLARGE; if (dpi > 0) { newMetrics.density = dpi / 160f; newMetrics.densityDpi = dpi; if (Build.VERSION.SDK_INT >= 17) setIntField(newConfig, "densityDpi", dpi); } if (fontScale > 0) newConfig.fontScale = fontScale / 100.0f; } } // ... whereas the locale is taken from the app for which resources are loaded if (packageName != null && isActive(packageName)) { Locale loc = getPackageSpecificLocale(packageName); if (loc != null) { if (newConfig == null) newConfig = new Configuration((Configuration) param.args[0]); setConfigurationLocale(newConfig, loc); // Also set the locale as the app-wide default, // for purposes other than resource loading // Only do this when the package's res are not being loaded by a different // host, regardless of the Widgets setting if (isActiveApp) Locale.setDefault(loc); } } if (newConfig != null) param.args[0] = newConfig; } }); } catch (Throwable t) { XposedBridge.log(t); } try { final int sdk = Build.VERSION.SDK_INT; XC_MethodHook notifyHook = new XC_MethodHook() { @SuppressWarnings("deprecation") @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { String packageName = (String) param.args[0]; Notification n; if (sdk <= 15 || sdk >= 18) n = (Notification) param.args[6]; else n = (Notification) param.args[5]; prefs.reload(); if (!isActive(packageName)) return; if (isActive(packageName, Common.PREF_INSISTENT_NOTIF)) { n.flags |= Notification.FLAG_INSISTENT; } if (isActive(packageName, Common.PREF_NO_BIG_NOTIFICATIONS)) { try { setObjectField(n, "bigContentView", null); } catch (Exception e) { } } int ongoingNotif = XposedMod.prefs.getInt(packageName + Common.PREF_ONGOING_NOTIF, Common.ONGOING_NOTIF_DEFAULT); if (ongoingNotif == Common.ONGOING_NOTIF_FORCE) { n.flags |= Notification.FLAG_ONGOING_EVENT; } else if (ongoingNotif == Common.ONGOING_NOTIF_PREVENT) { n.flags &= ~Notification.FLAG_ONGOING_EVENT & ~Notification.FLAG_FOREGROUND_SERVICE; } if (isActive(packageName, Common.PREF_MUTE)) { n.sound = null; n.flags &= ~Notification.DEFAULT_SOUND; } if (sdk >= 16 && isActive(packageName) && prefs.contains(packageName + Common.PREF_NOTIF_PRIORITY)) { int priority = XposedMod.prefs.getInt(packageName + Common.PREF_NOTIF_PRIORITY, 0); if (priority > 0 && priority < Common.notifPriCodes.length) { n.flags &= ~Notification.FLAG_HIGH_PRIORITY; n.priority = Common.notifPriCodes[priority]; } } } }; if (sdk <= 15) { findAndHookMethod("com.android.server.NotificationManagerService", null, "enqueueNotificationInternal", String.class, int.class, int.class, String.class, int.class, int.class, Notification.class, int[].class, notifyHook); } else if (sdk == 16) { findAndHookMethod("com.android.server.NotificationManagerService", null, "enqueueNotificationInternal", String.class, int.class, int.class, String.class, int.class, Notification.class, int[].class, notifyHook); } else if (sdk == 17) { findAndHookMethod("com.android.server.NotificationManagerService", null, "enqueueNotificationInternal", String.class, int.class, int.class, String.class, int.class, Notification.class, int[].class, int.class, notifyHook); } else if (sdk >= 18) { findAndHookMethod("com.android.server.NotificationManagerService", null, "enqueueNotificationInternal", String.class, String.class, int.class, int.class, String.class, int.class, Notification.class, int[].class, int.class, notifyHook); } } catch (Throwable t) { XposedBridge.log(t); } PackagePermissions.initHooks(); Activities.hookActivitySettings(); } @SuppressLint("NewApi") private void setConfigurationLocale(Configuration config, Locale loc) { config.locale = loc; if (Build.VERSION.SDK_INT >= 17) { // Don't use setLocale() in order not to trigger userSetLocale config.setLayoutDirection(loc); } } /** Adjust all framework dimensions that should reflect * changes related with SystemUI, namely statusbar and navbar sizes. * The values are adjusted and replaced system-wide by fixed px values. */ private void adjustSystemDimensions() { if (!isActive(SYSTEMUI_PACKAGE)) return; int systemUiDpi = prefs.getInt(SYSTEMUI_PACKAGE + Common.PREF_DPI, prefs.getInt(Common.PREF_DEFAULT + Common.PREF_DPI, 0)); if (systemUiDpi <= 0) return; // SystemUI had its DPI overridden. // Adjust the relevant framework dimen resources. Resources sysRes = Resources.getSystem(); float sysDensity = sysRes.getDisplayMetrics().density; float scaleFactor = (systemUiDpi / 160f) / sysDensity; for (String resName : SYSTEMUI_ADJUSTED_DIMENSIONS) { int id = sysRes.getIdentifier(resName, "dimen", "android"); if (id != 0) { float original = sysRes.getDimension(id); XResources.setSystemWideReplacement(id, new DimensionReplacement(original * scaleFactor, TypedValue.COMPLEX_UNIT_PX)); } } } @Override public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { prefs.reload(); // Override the default Locale if one is defined (not res-related, here) if (isActive(lpparam.packageName)) { Locale packageLocale = getPackageSpecificLocale(lpparam.packageName); if (packageLocale != null) Locale.setDefault(packageLocale); } if (this_package.equals(lpparam.packageName)) { findAndHookMethod("de.robv.android.xposed.mods.appsettings.XposedModActivity", lpparam.classLoader, "isModActive", XC_MethodReplacement.returnConstant(true)); } try { if (isActive(lpparam.packageName, Common.PREF_LEGACY_MENU)) findAndHookMethod(ViewConfiguration.class, "hasPermanentMenuKey", XC_MethodReplacement.returnConstant(true)); } catch (Throwable t) { XposedBridge.log(t); } try { if (isActive(lpparam.packageName, Common.PREF_MUTE)) { // Hook the AudioTrack API findAndHookMethod(AudioTrack.class, "play", XC_MethodReplacement.returnConstant(null)); // Hook the JetPlayer API findAndHookMethod(JetPlayer.class, "play", XC_MethodReplacement.returnConstant(null)); // Hook the MediaPlayer API XC_MethodHook displayHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { // Detect if video will be used for this media if (param.args[0] != null) setAdditionalInstanceField(param.thisObject, "HasVideo", true); else removeAdditionalInstanceField(param.thisObject, "HasVideo"); } }; findAndHookMethod(MediaPlayer.class, "setSurface", Surface.class, displayHook); findAndHookMethod(MediaPlayer.class, "setDisplay", SurfaceHolder.class, displayHook); findAndHookMethod(MediaPlayer.class, "start", new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { if (getAdditionalInstanceField(param.thisObject, "HasVideo") != null) // Video will be used - still start the media but with muted volume ((MediaPlayer) param.thisObject).setVolume(0, 0); else // No video - skip starting to play the media altogether param.setResult(null); } }); // Hook the SoundPool API findAndHookMethod(SoundPool.class, "play", int.class, float.class, float.class, int.class, int.class, float.class, XC_MethodReplacement.returnConstant(0)); findAndHookMethod(SoundPool.class, "resume", int.class, XC_MethodReplacement.returnConstant(null)); } } catch (Throwable t) { XposedBridge.log(t); } } private static Locale getPackageSpecificLocale(String packageName) { String locale = prefs.getString(packageName + Common.PREF_LOCALE, null); if (locale == null || locale.isEmpty()) return null; String[] localeParts = locale.split("_", 3); String language = localeParts[0]; String region = (localeParts.length >= 2) ? localeParts[1] : ""; String variant = (localeParts.length >= 3) ? localeParts[2] : ""; return new Locale(language, region, variant); } public static void loadPrefs() { prefs = new XSharedPreferences(Common.MY_PACKAGE_NAME, Common.PREFS); prefs.makeWorldReadable(); } public static boolean isActive(String packageName) { return prefs.getBoolean(packageName + Common.PREF_ACTIVE, false); } public static boolean isActive(String packageName, String sub) { return prefs.getBoolean(packageName + Common.PREF_ACTIVE, false) && prefs.getBoolean(packageName + sub, false); } }