├── hidden-api ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── android │ │ ├── view │ │ ├── SurfaceControl$StaticDisplayInfo.java │ │ ├── SurfaceControlHidden34.java │ │ └── SurfaceControlHiddenUpto33.java │ │ ├── content │ │ └── pm │ │ │ └── IPackageManager.java │ │ └── os │ │ └── ServiceManager.java ├── build.gradle └── proguard-rules.pro ├── app ├── .gitignore ├── src │ └── main │ │ ├── assets │ │ └── xposed_init │ │ ├── res │ │ ├── values │ │ │ ├── arrays.xml │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ │ └── strings.xml │ │ ├── xml │ │ │ └── root_preferences.xml │ │ └── layout │ │ │ └── settings_activity.xml │ │ ├── aidl │ │ └── xyz │ │ │ └── cirno │ │ │ └── pseudodcdimming │ │ │ ├── IBacklightOverrideStateListener.aidl │ │ │ ├── ServiceDiscoveryResult.aidl │ │ │ ├── BacklightOverridePreference.aidl │ │ │ └── IBacklightOverrideService.aidl │ │ ├── java │ │ └── xyz │ │ │ └── cirno │ │ │ └── pseudodcdimming │ │ │ ├── xposed │ │ │ ├── BacklightAdapterProxy.java │ │ │ ├── BacklightOverrideState.java │ │ │ ├── DisplayDeviceConfigProxy.java │ │ │ ├── BacklightOverridePreferenceLocal.java │ │ │ ├── SurfaceControlCompat.java │ │ │ ├── DisplayTransformManagerProxy.java │ │ │ ├── DisplayControlProxy.java │ │ │ ├── XposedInit.java │ │ │ └── BacklightOverrideService.java │ │ │ ├── util │ │ │ └── PerceptualQuantizer.java │ │ │ ├── HandlerTimer.java │ │ │ ├── ServiceDiscovery.java │ │ │ ├── BacklightRequest.java │ │ │ └── SettingsActivity.java │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── misc.xml └── gradle.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── details.zh.md ├── README.zh.md ├── settings.gradle ├── details.md ├── gradle.properties ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /hidden-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hidden-api/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | xyz.cirno.pseudodcdimming.xposed.XposedInit -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dantmnf/PseudoDCDimming/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | 5 | -------------------------------------------------------------------------------- /hidden-api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jun 08 19:36:21 HKT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /app/src/main/aidl/xyz/cirno/pseudodcdimming/IBacklightOverrideStateListener.aidl: -------------------------------------------------------------------------------- 1 | // IBacklightOverrideStateListener.aidl 2 | package xyz.cirno.pseudodcdimming; 3 | 4 | parcelable BacklightRequest; 5 | 6 | interface IBacklightOverrideStateListener { 7 | oneway void onBacklightUpdated(in BacklightRequest request, in BacklightRequest override, float gain); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/aidl/xyz/cirno/pseudodcdimming/ServiceDiscoveryResult.aidl: -------------------------------------------------------------------------------- 1 | // ServiceDiscoveryResult.aidl 2 | package xyz.cirno.pseudodcdimming; 3 | 4 | import xyz.cirno.pseudodcdimming.IBacklightOverrideService; 5 | // Declare any non-default types here with import statements 6 | 7 | parcelable ServiceDiscoveryResult { 8 | int version; 9 | IBinder service; 10 | } 11 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/view/SurfaceControl$StaticDisplayInfo.java: -------------------------------------------------------------------------------- 1 | package android.view; 2 | 3 | import android.hardware.display.DeviceProductInfo; 4 | 5 | public final class SurfaceControl$StaticDisplayInfo { 6 | public boolean isInternal; 7 | public float density; 8 | public boolean secure; 9 | public DeviceProductInfo deviceProductInfo; 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/aidl/xyz/cirno/pseudodcdimming/BacklightOverridePreference.aidl: -------------------------------------------------------------------------------- 1 | // BacklightOverridePreference.aidl 2 | package xyz.cirno.pseudodcdimming; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | parcelable BacklightOverridePreference { 7 | boolean enabled; 8 | float minimumOverrideBacklightLevel; 9 | boolean duplicateApplicationWorkaround; 10 | } -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/view/SurfaceControlHidden34.java: -------------------------------------------------------------------------------- 1 | package android.view; 2 | 3 | import android.os.IBinder; 4 | 5 | import dev.rikka.tools.refine.RefineAs; 6 | 7 | @RefineAs(SurfaceControl.class) 8 | public final class SurfaceControlHidden34 { 9 | public static SurfaceControl$StaticDisplayInfo getStaticDisplayInfo(long displayId) { 10 | throw new RuntimeException(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/content/pm/IPackageManager.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public interface IPackageManager extends android.os.IInterface { 4 | String getNameForUid(int uid); 5 | public static abstract class Stub extends android.os.Binder implements IPackageManager { 6 | public static IPackageManager asInterface(android.os.IBinder obj) { 7 | throw new RuntimeException(); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/os/ServiceManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | public final class ServiceManager { 4 | public static IBinder getService(String name) { 5 | throw new RuntimeException(); 6 | } 7 | public static IBinder waitForService(String name) { 8 | throw new RuntimeException(); 9 | } 10 | public static String[] listServices() { 11 | throw new RuntimeException(); 12 | } 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /hidden-api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | 5 | android { 6 | namespace 'xyz.cirno.pseudodcbacklight.hiddenapi' 7 | 8 | compileSdk 32 9 | 10 | defaultConfig { 11 | minSdk 32 12 | targetSdk 32 13 | 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_8 18 | targetCompatibility JavaVersion.VERSION_1_8 19 | } 20 | } 21 | 22 | dependencies { 23 | annotationProcessor 'dev.rikka.tools.refine:annotation-processor:4.3.0' 24 | compileOnly 'dev.rikka.tools.refine:annotation:4.3.0' 25 | } -------------------------------------------------------------------------------- /details.zh.md: -------------------------------------------------------------------------------- 1 | ## 详细原理 2 | 3 | 当前的 OLED 屏幕具有独立的亮度控制输入,用于控制在最高信号电平输入下的亮度。因此,如果我们想要显示亮度为 20 cd/m2 的白色,以下两种方式都可以实现: 4 | 5 | 1. 调整亮度控制输入,使信号输入电平最高时的亮度为 20 cd/m2,然后输入最高电平信号(“白色”); 6 | 2. 在当前具有更高的亮度控制输入时,比如 160 cd/m2,输入一个较低的信号电平(“灰色”),产生 12.5% 最高亮度(20 cd/m2)的输出。 7 | 8 | 部分 OLED 屏幕在第二种工作情况下会使用更高的 PWM 频率/占空比,一些用户认为这有助于减轻长时间或在黑暗环境下观看屏幕时的不适感。 9 | 10 | 信号电平与输出亮度之间不是线性关系,因此,第二种方式并不是简单的将输入信号乘以一个系数。但基于以下前提,我们可以实现精确的控制: 11 | 12 | 1. Android 系统假定屏幕具有 sRGB 响应曲线; 13 | 2. 基于 sRGB 响应曲线,系统框架及 HAL 提供在线性空间中的 RGB 矩阵变换; 14 | 3. 自 Android 12 开始,系统框架及 HAL 接口支持以 cd/m2(或称为 nits)为单位的绝对亮度控制。 15 | -------------------------------------------------------------------------------- /app/src/main/aidl/xyz/cirno/pseudodcdimming/IBacklightOverrideService.aidl: -------------------------------------------------------------------------------- 1 | // IBacklightOverrideService.aidl 2 | package xyz.cirno.pseudodcdimming; 3 | 4 | import xyz.cirno.pseudodcdimming.IBacklightOverrideStateListener; 5 | import xyz.cirno.pseudodcdimming.BacklightOverridePreference; 6 | 7 | interface IBacklightOverrideService { 8 | const int VERSION = 3; 9 | BacklightOverridePreference getPreference(); 10 | void putPreference(in BacklightOverridePreference pref); 11 | 12 | void registerBacklightOverrideStateListener(IBacklightOverrideStateListener listener); 13 | void removeBacklightOverrideStateListener(IBacklightOverrideStateListener listener); 14 | } 15 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/view/SurfaceControlHiddenUpto33.java: -------------------------------------------------------------------------------- 1 | package android.view; 2 | 3 | import android.os.IBinder; 4 | 5 | import dev.rikka.tools.refine.RefineAs; 6 | 7 | @RefineAs(SurfaceControl.class) 8 | public final class SurfaceControlHiddenUpto33 { 9 | public static SurfaceControl$StaticDisplayInfo getStaticDisplayInfo(IBinder displayToken) { 10 | throw new RuntimeException(); 11 | } 12 | public static long[] getPhysicalDisplayIds() { 13 | throw new RuntimeException(); 14 | } 15 | public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { 16 | throw new RuntimeException(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # 伪 DC 调光 2 | 3 | [English](README.md) | [下载最新版本](https://github.com/dantmnf/PseudoDCDimming/releases/latest) 4 | 5 | 通过软件增益为部分 OLED 屏幕在低亮度下启用类 DC 调光方式。 6 | 7 | 这是一个 Xposed (LSPosed) 模块,仅支持 Android 12 及以上版本。 8 | 9 | ## 原理 10 | 11 | 通过限制亮度控制的最小值,并通过矩阵变换功能缩小输出信号,使实际显示亮度匹配预期亮度,同时保持较高的 PWM 频率/占空比。 12 | 13 | [详细原理](details.zh.md) 14 | 15 | ## 限制 16 | 17 | * 严重依赖厂商对屏幕亮度控制以及响应曲线的校准。如果厂商在校准时使用了不同的响应曲线,或者亮度控制存在非线性行为,都可能影响开启模块后的显示质量; 18 | * 可能与其他颜色变换功能冲突; 19 | * 可能与 HDR 显示冲突。 20 | 21 | ## 配置 22 | 23 | 通过相机的高速快门模式放大频闪效应,或使用专业仪器测量 PWM 频率及占空比,选择一个可以接受的亮度值作为最小硬件亮度。 24 | 25 | ## 致谢 26 | 27 | 灵感来自 [ztc1997/FakeDCBacklight](https://github.com/ztc1997/FakeDCBacklight)。本项目额外实现了立即应用以及稳定启用前后亮度的功能。 28 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/BacklightAdapterProxy.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public class BacklightAdapterProxy { 6 | private final Object adapter; 7 | private final Method setBacklightMethod; 8 | 9 | public BacklightAdapterProxy(Object adapter, Method setBacklightMethod) { 10 | this.adapter = adapter; 11 | this.setBacklightMethod = setBacklightMethod; 12 | } 13 | 14 | public void setBacklight(float sdrBacklight, float sdrNits, float backlight, float nits) { 15 | try { 16 | setBacklightMethod.invoke(adapter, sdrBacklight, sdrNits, backlight, nits); 17 | } catch (Exception e) { 18 | // ignore 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | 14 | maven { 15 | url "https://api.xposed.info/" 16 | content { 17 | includeGroup("de.robv.android.xposed") 18 | } 19 | } 20 | } 21 | 22 | versionCatalogs { 23 | libs { 24 | version("refine", "4.3.0") 25 | plugin('refine', 'dev.rikka.tools.refine').versionRef('refine') 26 | } 27 | } 28 | } 29 | 30 | rootProject.name = "PseudoDcBacklight" 31 | include ':app' 32 | include ':hidden-api' 33 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /hidden-api/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/util/PerceptualQuantizer.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.util; 2 | 3 | public class PerceptualQuantizer { 4 | private static final double m1 = 2610.0 / 16384.0; 5 | private static final double m2 = 128.0 * 2523.0 / 4096.0; 6 | private static final double c1 = 3424.0 / 4096.0; 7 | private static final double c2 = 32.0 * 2413.0 / 4096.0; 8 | private static final double c3 = 32.0 * 2392.0 / 4096.0; 9 | 10 | public static double NitsToSignal(double nits) { 11 | var Ypowm1 = Math.pow(nits / 10000.0, m1); 12 | return Math.pow((c1 + c2 * Ypowm1) / (1.0 + c3 * Ypowm1), m2); 13 | } 14 | 15 | public static double SignalToNits(double signal) 16 | { 17 | var Epow1divm2 = Math.pow(signal, 1.0 / m2); 18 | return 10000 * Math.pow(Math.max(Epow1divm2 - c1, 0) / (c2 - c3 * Epow1divm2), 1 / m1); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/BacklightOverrideState.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import net.jcip.annotations.Immutable; 4 | 5 | import xyz.cirno.pseudodcdimming.BacklightRequest; 6 | 7 | @Immutable 8 | public final class BacklightOverrideState { 9 | public final BacklightRequest overrideRequest; 10 | public final float gain; 11 | public final BacklightOverridePreferenceLocal effectivePreference; 12 | public BacklightOverrideState(BacklightRequest overrideRequest, float gain, BacklightOverridePreferenceLocal effectivePreference) { 13 | this.overrideRequest = overrideRequest; 14 | this.gain = gain; 15 | this.effectivePreference = effectivePreference; 16 | } 17 | public static final BacklightOverrideState INVALID = new BacklightOverrideState(BacklightRequest.INVALID, 1.0f, BacklightOverridePreferenceLocal.DEFAULT); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/DisplayDeviceConfigProxy.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import de.robv.android.xposed.XposedHelpers; 6 | 7 | public class DisplayDeviceConfigProxy { 8 | private final Object obj; 9 | private final Method getNitsFromBacklightMethod; 10 | 11 | public DisplayDeviceConfigProxy(Object obj) { 12 | this.obj = obj; 13 | getNitsFromBacklightMethod = XposedHelpers.findMethodExact(obj.getClass(), "getNitsFromBacklight", float.class); 14 | } 15 | 16 | float getNitsFromBacklight(float backlight) { 17 | try { 18 | var value = getNitsFromBacklightMethod.invoke(obj, backlight); 19 | if (value != null) { 20 | return (float)value; 21 | } 22 | } catch (Exception e) { 23 | // ignore 24 | } 25 | return -1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/BacklightOverridePreferenceLocal.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import net.jcip.annotations.Immutable; 4 | 5 | @Immutable 6 | public final class BacklightOverridePreferenceLocal { 7 | public final boolean enabled; 8 | public final float minimumOverrideBacklightLevel; 9 | public final float minimumOverrideBacklightNits; 10 | public final boolean duplicateApplicationWorkaround; 11 | public static final BacklightOverridePreferenceLocal DEFAULT = new BacklightOverridePreferenceLocal(false, 0.0f, 0.0f, false); 12 | 13 | public BacklightOverridePreferenceLocal(boolean enabled, float minimumOverrideBacklightLevel, float minimumOverrideBacklightNits, boolean duplicateApplicationWorkaround) { 14 | this.enabled = enabled; 15 | this.minimumOverrideBacklightLevel = minimumOverrideBacklightLevel; 16 | this.minimumOverrideBacklightNits = minimumOverrideBacklightNits; 17 | this.duplicateApplicationWorkaround = duplicateApplicationWorkaround; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/SurfaceControlCompat.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import android.os.Build; 4 | import android.os.IBinder; 5 | import android.view.SurfaceControl$StaticDisplayInfo; 6 | import android.view.SurfaceControlHidden34; 7 | import android.view.SurfaceControlHiddenUpto33; 8 | 9 | public class SurfaceControlCompat { 10 | public static SurfaceControl$StaticDisplayInfo getStaticDisplayInfo(IBinder displayToken) { 11 | if (Build.VERSION.SDK_INT >= 34) { 12 | var dc = DisplayControlProxy.getInstance(); 13 | for (var physicalDisplayId : dc.getPhysicalDisplayIds()) { 14 | if (displayToken.equals(dc.getPhysicalDisplayToken(physicalDisplayId))) { 15 | return SurfaceControlHidden34.getStaticDisplayInfo(physicalDisplayId); 16 | } 17 | } 18 | throw new RuntimeException("Display token not found: " + displayToken); 19 | } else { 20 | return SurfaceControlHiddenUpto33.getStaticDisplayInfo(displayToken); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/HandlerTimer.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming; 2 | 3 | import android.os.Handler; 4 | 5 | public final class HandlerTimer { 6 | private Handler handler; 7 | private Runnable callback; 8 | private long intervalMillis; 9 | 10 | private Runnable wrapper; 11 | 12 | public HandlerTimer(Handler handler, long intervalMillis, Runnable callback) { 13 | this.handler = handler; 14 | this.callback = callback; 15 | this.intervalMillis = intervalMillis; 16 | 17 | wrapper = new Runnable() { 18 | @Override 19 | public void run() { 20 | callback.run(); 21 | handler.postDelayed(this, HandlerTimer.this.intervalMillis); 22 | } 23 | }; 24 | } 25 | 26 | public void start() { 27 | handler.postDelayed(wrapper, intervalMillis); 28 | } 29 | 30 | public void stop() { 31 | handler.removeCallbacks(wrapper); 32 | } 33 | 34 | public void setIntervalMillis(long interval) { 35 | this.intervalMillis = interval; 36 | } 37 | 38 | public long getIntervalMillis() { 39 | return intervalMillis; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/DisplayTransformManagerProxy.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public class DisplayTransformManagerProxy { 6 | private final Object obj; 7 | private Method setColorMatrixMethod; 8 | private Method needsLinearColorMatrixMethod; 9 | 10 | public DisplayTransformManagerProxy(Object obj) { 11 | this.obj = obj; 12 | try { 13 | setColorMatrixMethod = obj.getClass().getMethod("setColorMatrix", int.class, float[].class); 14 | needsLinearColorMatrixMethod = obj.getClass().getMethod("needsLinearColorMatrix"); 15 | } catch (NoSuchMethodException ignore) {} 16 | } 17 | public void setColorMatrix(int level, float[] value) { 18 | if (setColorMatrixMethod == null) return; 19 | try { 20 | setColorMatrixMethod.invoke(obj, level, value); 21 | } catch (Exception ignored) {} 22 | } 23 | public boolean needsLinearColorMatrix() { 24 | if (setColorMatrixMethod == null) return true; 25 | try { 26 | return (boolean)needsLinearColorMatrixMethod.invoke(obj); 27 | } catch (Exception e) { 28 | return false; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 27 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id('dev.rikka.tools.refine') version '4.3.0' 4 | } 5 | 6 | android { 7 | compileSdk 33 8 | 9 | defaultConfig { 10 | applicationId "xyz.cirno.pseudodcdimming" 11 | minSdk 31 12 | targetSdk 33 13 | versionCode 7 14 | versionName "1.3" 15 | 16 | } 17 | applicationVariants.all { variant -> 18 | variant.resValue "string", "app_version_name", variant.versionName 19 | } 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | buildFeatures { 27 | aidl true 28 | } 29 | compileOptions { 30 | //coreLibraryDesugaringEnabled true 31 | 32 | sourceCompatibility JavaVersion.VERSION_17 33 | targetCompatibility JavaVersion.VERSION_17 34 | } 35 | 36 | namespace 'xyz.cirno.pseudodcdimming' 37 | } 38 | 39 | dependencies { 40 | implementation 'net.jcip:jcip-annotations:1.0' 41 | compileOnly('androidx.annotation:annotation-jvm:1.6.0') 42 | compileOnly project(path: ':hidden-api') 43 | compileOnly("de.robv.android.xposed:api:82") 44 | // coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' 45 | } -------------------------------------------------------------------------------- /details.md: -------------------------------------------------------------------------------- 1 | ## How it works (detailed) 2 | 3 | Current OLED screens have a separate brightness control input to control brightness at the highest input signal. If we want to display white at a brightness of 20 cd/m2, we can do either of the following: 4 | 5 | 1. Adjust the brightness control so that the brightness at the highest input signal is 20 cd/m2, then use the highest signal ("white") as input; 6 | 2. With a higher brightness control input, say 160 cd/m2, use a lower signal ("gray") as input to produce 12.5% of maximum brightness (20 cd/m2 m2) output. 7 | 8 | Some OLED screens use a higher PWM frequency and duty cycle in the latter case, which some users feel helps reduce discomfort when viewing the screen for a long time or in dark environments. 9 | 10 | The relationship between signal level and output brightness is not linear, so the second method is not simply multiplying the signal by a factor. However, given the following premises, we can achieve precise control: 11 | 12 | 1. The Android system assumes that the screen has an sRGB response curve; 13 | 2. Based on the sRGB response curve, the system framework and HAL provide RGB matrix transformation in linear space; 14 | 3. Starting from Android 12, the system framework and HAL interface support absolute brightness control in cd/m2 (or nits). 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 伪 DC 调光 4 | 在 LSPosed 中启用模块并重新启动应用以保存设置。 5 | 通过软件亮度增益为部分 OLED 屏幕在低亮度下启用替代(类 DC)调制方式。 6 | 模块设置 7 | 设置 8 | 启用 9 | 最低硬件亮度 10 | 状态 11 | 目标亮度 12 | 硬件亮度 13 | 软件亮度(增益) 14 | 服务未运行 15 | 在 LSPosed 中启用模块并重新启动设备。 16 | 无法保存设置 17 | 服务版本不匹配 18 | 重新启动设备以激活更新后的服务。 19 | 关于 20 | 版本 21 | 项目主页 22 | 修复重复应用的增益 23 | 一些设备会错误地应用两次增益,我们可以通过发送增益的平方根来绕过这个问题。 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Enables namespacing of each library's R class so that its R class includes only the 19 | # resources declared in the library itself and none from the library's dependencies, 20 | # thereby reducing the size of the R class for that library 21 | android.nonTransitiveRClass=true 22 | android.defaults.buildfeatures.buildconfig=true 23 | android.nonFinalResIds=false 24 | android.enableR8.fullMode=false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pseudo DC Dimming 2 | 3 | [简体中文](README.zh.md) | [下载最新版本](https://github.com/dantmnf/PseudoDCDimming/releases/latest) 4 | 5 | Enable alternative dimming mode (likely DC-like) on low brightness for some OLED displays by using software brightness gain. 6 | 7 | Requires Android 12+ and Xposed-compatible framework. 8 | 9 | [Download latest release](https://github.com/dantmnf/PseudoDCDimming/releases/latest) 10 | 11 | ## How it works 12 | 13 | By limiting the minimum brightness and scaling down the output signal through a degamma-gain-regamma transform, the actual display brightness matches the expected brightness while maintaining a higher PWM frequency and duty cycle. 14 | 15 | [details](details.md) 16 | 17 | ## Limitations 18 | 19 | * Rely heavily on the manufacturer's calibration of screen brightness controls and response curves. If the manufacturer uses different response curves during calibration, or the brightness control has non-linear behavior, it may affect the display quality after turning on the module; 20 | * May conflict with other color transform functions; 21 | * May conflict with HDR content. 22 | 23 | ## Configuration 24 | 25 | Use the high-speed shutter mode of the camera to amplify the stroboscopic effect, or use professional instruments to measure the PWM frequency and duty cycle. Choose an acceptable brightness value as the minimum hardware brightness. 26 | 27 | ## Acknowledgements 28 | 29 | Inspired from [ztc1997/FakeDCBacklight](https://github.com/ztc1997/FakeDCBacklight). This project additionally implements immediate application as well as stabilizing the brightness before and after enabling. 30 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/DisplayControlProxy.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.IBinder; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | @SuppressLint("PrivateApi") 9 | public class DisplayControlProxy { 10 | private static DisplayControlProxy instance; 11 | private Method getPhysicalDisplayIdsMethod; 12 | private Method getPhysicalDisplayTokenMethod; 13 | 14 | public static void initialize(ClassLoader systemServerClassLoader) { 15 | if (instance == null) { 16 | instance = new DisplayControlProxy(systemServerClassLoader); 17 | } 18 | } 19 | 20 | public static DisplayControlProxy getInstance() { 21 | return instance; 22 | } 23 | 24 | private DisplayControlProxy(ClassLoader systemServerClassLoader) { 25 | try { 26 | final var clazz = systemServerClassLoader.loadClass("com.android.server.display.DisplayControl"); 27 | getPhysicalDisplayIdsMethod = clazz.getDeclaredMethod("getPhysicalDisplayIds"); 28 | getPhysicalDisplayTokenMethod = clazz.getDeclaredMethod("getPhysicalDisplayToken", long.class); 29 | } catch (Exception e) { 30 | // ignore 31 | } 32 | } 33 | 34 | public long[] getPhysicalDisplayIds() { 35 | try { 36 | return (long[])getPhysicalDisplayIdsMethod.invoke(null); 37 | } catch (Exception e) { 38 | return new long[0]; 39 | } 40 | } 41 | 42 | public IBinder getPhysicalDisplayToken(long physicalDisplayId) { 43 | try { 44 | return (IBinder)getPhysicalDisplayTokenMethod.invoke(null, physicalDisplayId); 45 | } catch (Exception e) { 46 | return null; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pseudo DC Dimming 3 | 4 | Module Settings 5 | 6 | 7 | Settings 8 | Enable 9 | Minimum HAL brightness 10 | Status 11 | Target brightness 12 | HAL brightness 13 | Software brightness (gain) 14 | 15 | Service not running 16 | Enable this module in LSPosed and restart. 17 | 18 | Service version mismatch 19 | Restart the device to activate updated service. 20 | 21 | Settings cannot be saved 22 | Enable the module in LSPosed and restart this app to save settings. 23 | 24 | Enable alternative modulation (likely DC-like) on low brightness for some OLED displays by using software brightness gain. 25 | 26 | 27 | 28 | About 29 | Version 30 | Project home 31 | https://github.com/dantmnf/PseudoDCDimming 32 | Duplicated gain workaround 33 | Some devices will erroneously apply the gain twice, we can get around this by sending the square root of the gain. 34 | -------------------------------------------------------------------------------- /app/src/main/res/xml/root_preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 50 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/settings_activity.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 23 | 24 | 32 | 33 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/ServiceDiscovery.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming; 2 | 3 | import android.content.Context; 4 | import android.os.IBinder; 5 | import android.os.Parcel; 6 | import android.os.ServiceManager; 7 | import android.util.Log; 8 | 9 | public class ServiceDiscovery { 10 | public static final int TRANSACTION_SERVICE_DISCOVERY = 0x67000000 | 114514; 11 | private static boolean _isVersionMismatch = false; 12 | private static IBinder displayManager; 13 | 14 | private static IBinder getDisplayManager() { 15 | if (displayManager == null) { 16 | try { 17 | displayManager = ServiceManager.getService(Context.DISPLAY_SERVICE); 18 | } catch (Exception e) { 19 | Log.e("IPCUtil", "failed to call ServiceManager.getService, module not enabled?"); 20 | } 21 | } 22 | return displayManager; 23 | } 24 | 25 | public static boolean isVersionMismatch() { 26 | if (displayManager == null) { 27 | getDisplayManager(); 28 | } 29 | return _isVersionMismatch; 30 | } 31 | 32 | public static IBacklightOverrideService getService() { 33 | final var ds = getDisplayManager(); 34 | if (ds == null) { 35 | return null; 36 | } 37 | 38 | final var req = Parcel.obtain(); 39 | final var resp = Parcel.obtain(); 40 | try { 41 | var status = ds.transact(TRANSACTION_SERVICE_DISCOVERY, req, resp, 0); 42 | if (status) { 43 | var result2 = ServiceDiscoveryResult.CREATOR.createFromParcel(resp); 44 | if (result2.version == 0) { 45 | return null; 46 | } 47 | if (result2.version != IBacklightOverrideService.VERSION) { 48 | _isVersionMismatch = true; 49 | return null; 50 | } 51 | return IBacklightOverrideService.Stub.asInterface(result2.service); 52 | } 53 | } catch (Exception e) { 54 | // ignore 55 | } finally { 56 | req.recycle(); 57 | resp.recycle(); 58 | } 59 | return null; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/BacklightRequest.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import net.jcip.annotations.Immutable; 9 | 10 | @Immutable 11 | public final class BacklightRequest implements Parcelable { 12 | public final float sdrBacklightLevel; 13 | public final float sdrBacklightNits; 14 | public final float backlightLevel; 15 | public final float backlightNits; 16 | 17 | public BacklightRequest(float sdrBacklightLevel, float sdrBacklightNits, float backlightLevel, float backlightNits) { 18 | this.sdrBacklightLevel = sdrBacklightLevel; 19 | this.sdrBacklightNits = sdrBacklightNits; 20 | this.backlightLevel = backlightLevel; 21 | this.backlightNits = backlightNits; 22 | } 23 | 24 | public static final BacklightRequest INVALID = new BacklightRequest(Float.NaN, Float.NaN, Float.NaN, Float.NaN); 25 | 26 | // can't use AIDL source generator because we need to make it immutable (shrug 27 | public static final Creator CREATOR = new Creator<>() { 28 | @Override 29 | public BacklightRequest createFromParcel(Parcel in) { 30 | final var pos0 = in.dataPosition(); 31 | final var size = in.readInt(); 32 | float _backlightLevel = 0.0f; 33 | float _backlightNits = 0.0f; 34 | float _sdrBacklightLevel = 0.0f; 35 | float _sdrBacklightNits = 0.0f; 36 | 37 | if (size < 0) return INVALID; 38 | try { 39 | _backlightLevel = in.readFloat(); 40 | if (in.dataPosition() - pos0 >= size) return INVALID; 41 | _backlightNits = in.readFloat(); 42 | if (in.dataPosition() - pos0 >= size) return INVALID; 43 | _sdrBacklightLevel = in.readFloat(); 44 | if (in.dataPosition() - pos0 >= size) return INVALID; 45 | _sdrBacklightNits = in.readFloat(); 46 | if (in.dataPosition() - pos0 > size) return INVALID; 47 | } finally { 48 | in.setDataPosition(pos0 + size); 49 | } 50 | return new BacklightRequest(_backlightLevel, _backlightNits, _sdrBacklightLevel, _sdrBacklightNits); 51 | } 52 | 53 | @Override 54 | public BacklightRequest[] newArray(int size) { 55 | return new BacklightRequest[size]; 56 | } 57 | }; 58 | 59 | @Override 60 | public int describeContents() { 61 | return 0; 62 | } 63 | 64 | @Override 65 | public void writeToParcel(@NonNull Parcel dest, int flags) { 66 | final var pos0 = dest.dataPosition(); 67 | dest.writeInt(0); 68 | dest.writeFloat(backlightLevel); 69 | dest.writeFloat(backlightNits); 70 | dest.writeFloat(sdrBacklightLevel); 71 | dest.writeFloat(sdrBacklightNits); 72 | final var pos1 = dest.dataPosition(); 73 | dest.setDataPosition(pos0); 74 | dest.writeInt(pos1 - pos0); 75 | dest.setDataPosition(pos1); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.content.SharedPreferences; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import android.os.Handler; 10 | import android.os.Looper; 11 | import android.os.RemoteException; 12 | import android.os.SystemClock; 13 | import android.preference.CheckBoxPreference; 14 | import android.preference.EditTextPreference; 15 | import android.preference.Preference; 16 | import android.preference.PreferenceFragment; 17 | import android.preference.SwitchPreference; 18 | import android.util.Log; 19 | import android.widget.TextView; 20 | 21 | 22 | import java.util.Locale; 23 | 24 | @SuppressWarnings("deprecation") 25 | public class SettingsActivity extends Activity { 26 | private final static String TAG = "SettingsActivity"; 27 | private SettingsFragment fragment; 28 | private final Handler uiHandler = new Handler(Looper.myLooper()); 29 | private final HandlerTimer updateTimer = new HandlerTimer(uiHandler, 100, this::updateStatus); 30 | 31 | private IBacklightOverrideService service; 32 | private IBacklightOverrideStateListener listener; 33 | private SharedPreferences xsp = null; 34 | private static final int ERROR_VERSION_MISMATCH = 0x4; 35 | private static final int ERROR_SERVICE_NOT_FOUND = 0x2; 36 | private static final int ERROR_NO_PREFS = 0x1; 37 | 38 | private BacklightRequest requestBacklight; 39 | private BacklightRequest overrideBacklight; 40 | private float gain; 41 | 42 | private long lastNotificationTime = 0; 43 | private long lastUpdatedNotificationTime = 0; 44 | 45 | @Override 46 | @SuppressLint("WorldReadableFiles") 47 | protected void onCreate(Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | setContentView(R.layout.settings_activity); 50 | 51 | service = ServiceDiscovery.getService(); 52 | try { 53 | xsp = getSharedPreferences("config", MODE_WORLD_READABLE); 54 | } catch (SecurityException e) { 55 | Log.e(TAG, "failed to get shared preferences", e); 56 | } 57 | fragment = new SettingsFragment(); 58 | getFragmentManager() 59 | .beginTransaction() 60 | .replace(R.id.settings, fragment) 61 | .commit(); 62 | listener = new IBacklightOverrideStateListener.Stub() { 63 | @Override 64 | public void onBacklightUpdated(BacklightRequest request, BacklightRequest override, float gain) throws RemoteException { 65 | lastNotificationTime = SystemClock.uptimeMillis(); 66 | requestBacklight = request; 67 | overrideBacklight = override; 68 | SettingsActivity.this.gain = gain; 69 | } 70 | }; 71 | checkErrors(); 72 | syncXSharedPreferences(); 73 | } 74 | 75 | private void checkErrors() { 76 | int errorLevel = 0; 77 | if (service == null) { 78 | errorLevel |= ERROR_SERVICE_NOT_FOUND; 79 | } 80 | if (ServiceDiscovery.isVersionMismatch()) { 81 | errorLevel |= ERROR_VERSION_MISMATCH; 82 | } 83 | if (xsp == null) { 84 | errorLevel |= ERROR_NO_PREFS; 85 | } 86 | 87 | if ((errorLevel & ERROR_VERSION_MISMATCH) != 0) { 88 | setError(R.string.version_mismatch_title, R.string.version_mismatch_message); 89 | } else if ((errorLevel & ERROR_SERVICE_NOT_FOUND) != 0) { 90 | setError(R.string.not_loaded_title, R.string.not_loaded_message); 91 | } else if ((errorLevel & ERROR_NO_PREFS) != 0) { 92 | setError(R.string.no_prefs_title, R.string.no_prefs_message); 93 | } else { 94 | findViewById(R.id.errorBanner).setVisibility(android.view.View.GONE); 95 | } 96 | } 97 | 98 | private void setError(int titleId, int messageId) { 99 | ((TextView)findViewById(R.id.errorTitle)).setText(titleId); 100 | ((TextView)findViewById(R.id.errorMessage)).setText(messageId); 101 | findViewById(R.id.errorBanner).setVisibility(android.view.View.VISIBLE); 102 | } 103 | 104 | private void syncXSharedPreferences() { 105 | if (service == null || xsp == null) return; 106 | BacklightOverridePreference pref; 107 | try { 108 | pref = service.getPreference(); 109 | } catch (RemoteException e) { 110 | return; 111 | } 112 | boolean dirty = xsp.getBoolean("enabled", false) != pref.enabled; 113 | dirty = dirty || xsp.getFloat("minimum_brightness", 0.0f) != pref.minimumOverrideBacklightLevel; 114 | dirty = dirty || xsp.getBoolean("gain_applied_twice", false) != pref.duplicateApplicationWorkaround; 115 | 116 | if (dirty) { 117 | writeXSharedPreferences(pref); 118 | } 119 | } 120 | 121 | private void writeXSharedPreferences(BacklightOverridePreference pref) { 122 | if (xsp == null) return; 123 | xsp.edit() 124 | .putBoolean("enabled", pref.enabled) 125 | .putFloat("minimum_brightness", pref.minimumOverrideBacklightLevel) 126 | .putBoolean("gain_applied_twice", pref.duplicateApplicationWorkaround) 127 | .apply(); 128 | } 129 | private void updateStatus() { 130 | if (lastUpdatedNotificationTime == lastNotificationTime) return; 131 | fragment.updateStatus(requestBacklight, overrideBacklight, gain); 132 | lastUpdatedNotificationTime = lastNotificationTime; 133 | } 134 | @Override 135 | protected void onResume() { 136 | super.onResume(); 137 | if (service != null) { 138 | try { 139 | service.registerBacklightOverrideStateListener(listener); 140 | updateTimer.start(); 141 | } catch (RemoteException e) { 142 | Log.e(TAG, "failed to register listener", e); 143 | } 144 | } 145 | } 146 | 147 | @Override 148 | protected void onPause() { 149 | super.onPause(); 150 | if (service != null) { 151 | try { 152 | updateTimer.stop(); 153 | service.removeBacklightOverrideStateListener(listener); 154 | } catch (RemoteException e) { 155 | Log.e(TAG, "failed to unregister listener", e); 156 | } 157 | } 158 | } 159 | 160 | public static class SettingsFragment extends PreferenceFragment { 161 | private IBacklightOverrideService service; 162 | private SwitchPreference enablePref; 163 | private EditTextPreference minimumBrightnessPref; 164 | private CheckBoxPreference gainAppliedTwicePref; 165 | 166 | private Preference requestBacklightPref; 167 | private Preference overrideBacklightPref; 168 | private Preference gainPref; 169 | 170 | @Override 171 | public void onCreate(Bundle savedInstanceState) { 172 | super.onCreate(savedInstanceState); 173 | addPreferencesFromResource(R.xml.root_preferences); 174 | 175 | final var activity = (SettingsActivity) getActivity(); 176 | if (activity == null) return; 177 | service = activity.service; 178 | 179 | enablePref = (SwitchPreference) findPreference("enable"); 180 | if (enablePref != null) { 181 | enablePref.setOnPreferenceChangeListener((p, v) -> { 182 | try { 183 | final var pref = service.getPreference(); 184 | pref.enabled = (Boolean) v; 185 | service.putPreference(pref); 186 | activity.writeXSharedPreferences(pref); 187 | return true; 188 | } catch (RemoteException e) { 189 | return false; 190 | } 191 | }); 192 | } 193 | 194 | 195 | minimumBrightnessPref = (EditTextPreference) findPreference("minimum_brightness"); 196 | if (minimumBrightnessPref != null) { 197 | minimumBrightnessPref.setOnPreferenceChangeListener((p, v) -> { 198 | try { 199 | final var fvalue = Float.parseFloat((String) v) / 100.0f; 200 | if (fvalue > 1.0f || fvalue < 0.0f) return false; 201 | final var pref = service.getPreference(); 202 | pref.minimumOverrideBacklightLevel = fvalue; 203 | service.putPreference(pref); 204 | updateMinimumBrightnessPreference(fvalue); 205 | activity.writeXSharedPreferences(pref); 206 | return true; 207 | } catch (Exception e) { 208 | return false; 209 | } 210 | }); 211 | } 212 | 213 | gainAppliedTwicePref = (CheckBoxPreference) findPreference("gain_applied_twice"); 214 | if (gainAppliedTwicePref != null) { 215 | gainAppliedTwicePref.setOnPreferenceChangeListener((p, v) -> { 216 | try { 217 | final var pref = service.getPreference(); 218 | pref.duplicateApplicationWorkaround = (Boolean) v; 219 | service.putPreference(pref); 220 | activity.writeXSharedPreferences(pref); 221 | return true; 222 | } catch (Exception e) { 223 | return false; 224 | } 225 | }); 226 | } 227 | 228 | requestBacklightPref = findPreference("request_brightness"); 229 | overrideBacklightPref = findPreference("actual_brightness"); 230 | gainPref = findPreference("gain"); 231 | 232 | findPreference("project_home").setOnPreferenceClickListener(p -> { 233 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getResources().getString(R.string.project_url)))); 234 | return true; 235 | }); 236 | } 237 | 238 | @Override 239 | public void onResume() { 240 | super.onResume(); 241 | final SettingsActivity activity = (SettingsActivity) getActivity(); 242 | if (activity == null || activity.service == null) { 243 | findPreference("settings").setEnabled(false); 244 | findPreference("status").setEnabled(false); 245 | } else { 246 | try { 247 | var pref = service.getPreference(); 248 | enablePref.setChecked(pref.enabled); 249 | updateMinimumBrightnessPreference(pref.minimumOverrideBacklightLevel); 250 | gainAppliedTwicePref.setChecked(pref.duplicateApplicationWorkaround); 251 | } catch (Exception e) { 252 | // ignore 253 | } 254 | 255 | } 256 | } 257 | 258 | public void updateStatus(BacklightRequest request, BacklightRequest effective, float gain) { 259 | requestBacklightPref.setSummary(String.format(Locale.ROOT, "%.2f%% (%.2f cd/m²)", request.backlightLevel * 100, request.backlightNits)); 260 | overrideBacklightPref.setSummary(String.format(Locale.ROOT, "%.2f%% (%.2f cd/m²)", effective.backlightLevel * 100, effective.backlightNits)); 261 | gainPref.setSummary(String.format(Locale.ROOT, "%.5f", gain)); 262 | } 263 | 264 | private void updateMinimumBrightnessPreference(float newValue) { 265 | var s2 = String.format(Locale.ROOT, "%.2f", newValue * 100); 266 | minimumBrightnessPref.setText(s2); 267 | minimumBrightnessPref.setSummary(s2 + "%"); 268 | } 269 | 270 | } 271 | } -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/XposedInit.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import android.content.pm.IPackageManager; 4 | import android.os.Binder; 5 | import android.os.Build; 6 | import android.os.IBinder; 7 | import android.os.Parcel; 8 | import android.os.Parcelable; 9 | import android.os.ServiceManager; 10 | import android.util.Log; 11 | 12 | import java.util.Locale; 13 | 14 | import de.robv.android.xposed.IXposedHookLoadPackage; 15 | import de.robv.android.xposed.XC_MethodHook; 16 | import de.robv.android.xposed.XposedBridge; 17 | import de.robv.android.xposed.XposedHelpers; 18 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 19 | import xyz.cirno.pseudodcdimming.BacklightRequest; 20 | import xyz.cirno.pseudodcdimming.BuildConfig; 21 | import xyz.cirno.pseudodcdimming.IBacklightOverrideService; 22 | import xyz.cirno.pseudodcdimming.ServiceDiscovery; 23 | import xyz.cirno.pseudodcdimming.ServiceDiscoveryResult; 24 | 25 | public class XposedInit implements IXposedHookLoadPackage { 26 | private static final String TAG = "PseudoDcBacklight.Xposed"; 27 | private static final String ATTACHED_IS_INTERNAL = "xyz.cirno.pseudodcbacklight.isInternal"; 28 | @Override 29 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { 30 | if ("android".equals(lpparam.packageName)) { 31 | handleLoadSystemServer(lpparam); 32 | } 33 | } 34 | 35 | private void handleLoadSystemServer(XC_LoadPackage.LoadPackageParam lpparam) { 36 | final var classLoader = lpparam.classLoader; 37 | 38 | if (Build.VERSION.SDK_INT >= 34) { 39 | DisplayControlProxy.initialize(classLoader); 40 | } 41 | 42 | final var overrideService = new BacklightOverrideService(classLoader); 43 | 44 | final var localDisplayDevice = XposedHelpers.findClass("com.android.server.display.LocalDisplayAdapter$LocalDisplayDevice", classLoader); 45 | 46 | final var backlightAdapter = XposedHelpers.findClass("com.android.server.display.LocalDisplayAdapter$BacklightAdapter", classLoader); 47 | 48 | final var localDisplayAdapter = XposedHelpers.findClass( "com.android.server.display.LocalDisplayAdapter", classLoader); 49 | 50 | final var backlightAdapter_setBacklight = XposedHelpers.findMethodExact( 51 | backlightAdapter, 52 | "setBacklight", 53 | float.class, // sdrBacklight 54 | float.class, // sdrNits 55 | float.class, // backlight 56 | float.class // nits 57 | ); 58 | // Xiaomi HyperOS 2 compat 59 | var backlightAdapter_setBacklight_hookTarget = backlightAdapter_setBacklight; 60 | var miuiVersion = XposedHelpers.callStaticMethod( 61 | XposedHelpers.findClass("android.os.SystemProperties", classLoader), 62 | "get", 63 | "ro.mi.os.version.name" 64 | ); 65 | if("OS2.0".equals(miuiVersion)) { 66 | try { 67 | final var backlightAdapter_setBacklight_miui = XposedHelpers.findMethodExact( 68 | backlightAdapter, 69 | "setBacklight", 70 | float.class, // sdrBacklight 71 | float.class, // sdrNits 72 | float.class, // backlight 73 | float.class, // nits 74 | boolean.class // galleryHdrBoost 75 | ); 76 | Log.i(TAG, "HyperOS 2 detected, using dedicated hook target"); 77 | backlightAdapter_setBacklight_hookTarget = backlightAdapter_setBacklight_miui; 78 | } catch (NoSuchMethodError e) { 79 | Log.i(TAG, "HyperOS 2 detected, but the HyperOS-specific hook target couldn't be found; ignoring"); 80 | } 81 | } 82 | 83 | XposedHelpers.findAndHookConstructor(localDisplayDevice, 84 | localDisplayAdapter, // [surrounding this] 85 | IBinder.class, // displayToken 86 | long.class, // physicalDisplayId 87 | "android.view.SurfaceControl$StaticDisplayInfo", // staticDisplayInfo 88 | "android.view.SurfaceControl$DynamicDisplayInfo", // dynamicInfo 89 | "android.view.SurfaceControl$DesiredDisplayModeSpecs", // modeSpecs 90 | boolean.class, // isDefaultDisplay 91 | new XC_MethodHook() { 92 | @Override 93 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 94 | if (param.hasThrowable()) { 95 | return; 96 | } 97 | final var displayDeviceConfig = new DisplayDeviceConfigProxy(XposedHelpers.callMethod(param.thisObject,"getDisplayDeviceConfig")); 98 | overrideService.lateInitialize(displayDeviceConfig); 99 | } 100 | } 101 | ); 102 | XposedHelpers.findAndHookConstructor(backlightAdapter, 103 | IBinder.class, 104 | boolean.class, 105 | XposedHelpers.findClass("com.android.server.display.LocalDisplayAdapter$SurfaceControlProxy", classLoader), 106 | new XC_MethodHook() { 107 | @Override 108 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 109 | if (param.hasThrowable()) { 110 | return; 111 | } 112 | final var token = (IBinder)param.args[0]; 113 | final var staticInfo = SurfaceControlCompat.getStaticDisplayInfo(token); 114 | if (staticInfo == null) return; 115 | XposedHelpers.setAdditionalInstanceField( 116 | param.thisObject, 117 | ATTACHED_IS_INTERNAL, 118 | staticInfo.isInternal 119 | ); 120 | if (staticInfo.isInternal) { 121 | overrideService.backlightAdapter = new BacklightAdapterProxy(param.thisObject, backlightAdapter_setBacklight); 122 | } 123 | } 124 | }); 125 | 126 | XposedBridge.hookMethod(backlightAdapter_setBacklight_hookTarget, new XC_MethodHook() { 127 | @Override 128 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 129 | // void setBacklight(float sdrBacklight, float sdrNits, float backlight, float nits) 130 | final var isInternal = XposedHelpers.getAdditionalInstanceField(param.thisObject, ATTACHED_IS_INTERNAL); 131 | if (isInternal == null || !((boolean)isInternal)) return; 132 | final var requestSdrBacklight = (float)param.args[0]; 133 | final var requestSdrNits = (float)param.args[1]; 134 | final var requestBacklight = (float)param.args[2]; 135 | final var requestNits = (float)param.args[3]; 136 | 137 | var request = new BacklightRequest(requestSdrBacklight, requestSdrNits, requestBacklight, requestNits); 138 | 139 | var prevOverride = overrideService.getLastBacklightOverrideState(); 140 | var overrideState = overrideService.getOverrideBacklightAndGain(request); 141 | 142 | var overrideBacklight = overrideState.overrideRequest; 143 | param.args[0] = overrideBacklight.sdrBacklightLevel; 144 | param.args[1] = overrideBacklight.sdrBacklightNits; 145 | param.args[2] = overrideBacklight.backlightLevel; 146 | param.args[3] = overrideBacklight.backlightNits; 147 | // HyperOS 2: leave galleryHdrBoost argument unchanged 148 | Log.d(TAG, String.format(Locale.ROOT, "setBacklight(%f->%f, %f->%f, %f->%f, %f->%f)", 149 | requestSdrBacklight, 150 | overrideBacklight.sdrBacklightLevel, 151 | requestSdrNits, 152 | overrideBacklight.sdrBacklightNits, 153 | requestBacklight, 154 | overrideBacklight.backlightLevel, 155 | requestNits, 156 | overrideBacklight.backlightNits)); 157 | 158 | if (overrideBacklight.backlightLevel > prevOverride.overrideRequest.backlightLevel) { 159 | // increased hardware brightness: 160 | // set gain -> darker 161 | // set brightness -> brighter 162 | param.getExtra().putFloat("setGainAfterBrightness", -1.0f); 163 | overrideService.setTransformGain(overrideState.gain); 164 | } else { 165 | // decreased hardware brightness: 166 | // set brightness -> darker 167 | // set gain -> brighter 168 | // a dark spike is more acceptable than a bright spike 169 | param.getExtra().putFloat("setGainAfterBrightness", overrideState.gain); 170 | } 171 | } 172 | 173 | @Override 174 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 175 | if (param.hasThrowable()) { 176 | return; 177 | } 178 | var lateGain = param.getExtra().getFloat("setGainAfterBrightness", -1.0f); 179 | if (lateGain > 0.0f) { 180 | overrideService.setTransformGain(lateGain); 181 | } 182 | overrideService.notifyAllListeners(); 183 | } 184 | }); 185 | 186 | final var binderServiceClass = XposedHelpers.findClass("com.android.server.display.DisplayManagerService$BinderService", classLoader); 187 | XposedHelpers.findAndHookMethod("android.hardware.display.IDisplayManager$Stub", classLoader, 188 | "onTransact", 189 | int.class, Parcel.class, Parcel.class, int.class, 190 | new XC_MethodHook() { 191 | @Override 192 | protected void beforeHookedMethod(MethodHookParam param) throws Throwable { 193 | if (!param.thisObject.getClass().equals(binderServiceClass)) return; 194 | 195 | final int code = (int)param.args[0]; 196 | 197 | if (code != ServiceDiscovery.TRANSACTION_SERVICE_DISCOVERY) return; 198 | 199 | final Parcel data = (Parcel)param.args[1]; 200 | final Parcel reply = (Parcel)param.args[2]; 201 | final int flags = (int)param.args[3]; 202 | 203 | 204 | final var pmb = ServiceManager.getService("package"); 205 | // PMS not initialized yet, skip hook. 206 | if (pmb == null) return; 207 | 208 | final int uid = Binder.getCallingUid(); 209 | final var pm = IPackageManager.Stub.asInterface(pmb); 210 | final var callingpackage = pm.getNameForUid(uid); 211 | 212 | if (!BuildConfig.APPLICATION_ID.equals(callingpackage)) return; 213 | 214 | if (code == ServiceDiscovery.TRANSACTION_SERVICE_DISCOVERY) { 215 | if (reply == null) { 216 | param.setResult(false); 217 | return; 218 | } 219 | final var response = new ServiceDiscoveryResult(); 220 | response.version = IBacklightOverrideService.VERSION; 221 | response.service = overrideService.binderService.asBinder(); 222 | response.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); 223 | param.setResult(true); 224 | } 225 | } 226 | }); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /app/src/main/java/xyz/cirno/pseudodcdimming/xposed/BacklightOverrideService.java: -------------------------------------------------------------------------------- 1 | package xyz.cirno.pseudodcdimming.xposed; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.RemoteException; 5 | import android.util.Log; 6 | 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Locale; 11 | import java.util.Objects; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | 15 | import de.robv.android.xposed.XSharedPreferences; 16 | import de.robv.android.xposed.XposedHelpers; 17 | import xyz.cirno.pseudodcdimming.BacklightOverridePreference; 18 | import xyz.cirno.pseudodcdimming.BacklightRequest; 19 | import xyz.cirno.pseudodcdimming.BuildConfig; 20 | import xyz.cirno.pseudodcdimming.IBacklightOverrideService; 21 | import xyz.cirno.pseudodcdimming.IBacklightOverrideStateListener; 22 | import xyz.cirno.pseudodcdimming.util.PerceptualQuantizer; 23 | 24 | 25 | public class BacklightOverrideService { 26 | private static final String TAG = "BacklightOverrideService"; 27 | private final ClassLoader systemServerClassLoader; 28 | public final IBacklightOverrideService binderService = new BinderService(); 29 | private volatile BacklightRequest lastBacklightRequest = BacklightRequest.INVALID; 30 | private volatile BacklightOverrideState lastBacklightOverride = BacklightOverrideState.INVALID; 31 | private volatile BacklightOverridePreferenceLocal preference = BacklightOverridePreferenceLocal.DEFAULT; 32 | public float deviceMinimumBacklightNits = Float.NaN; 33 | public BacklightAdapterProxy backlightAdapter; 34 | public DisplayDeviceConfigProxy displayDeviceConfig; 35 | private ExecutorService callbackExecutor; 36 | private final List callbacks = new ArrayList<>(); 37 | private DisplayTransformManagerProxy dtm; 38 | private final int gainLayer; 39 | 40 | public BacklightOverrideService(ClassLoader classLoader) { 41 | systemServerClassLoader = classLoader; 42 | gainLayer = resolveGainLayer(classLoader); 43 | } 44 | 45 | public void lateInitialize(DisplayDeviceConfigProxy deviceConfig) { 46 | callbackExecutor = Executors.newSingleThreadExecutor(); 47 | displayDeviceConfig = deviceConfig; 48 | deviceMinimumBacklightNits = deviceConfig.getNitsFromBacklight(0.0f); 49 | Log.d(TAG, String.format(Locale.ROOT, "deviceMinimumBacklightNits = %f", deviceMinimumBacklightNits)); 50 | setPreference(readPersistentPreference()); 51 | } 52 | 53 | private BacklightOverridePreference readPersistentPreference() { 54 | var pref = new BacklightOverridePreference(); 55 | try { 56 | var xsp = new XSharedPreferences(BuildConfig.APPLICATION_ID, "config"); 57 | pref.enabled = xsp.getBoolean("enabled", false); 58 | pref.minimumOverrideBacklightLevel = xsp.getFloat("minimum_brightness", 0.0f); 59 | pref.duplicateApplicationWorkaround = xsp.getBoolean("gain_applied_twice", false); 60 | } catch (Exception e) { 61 | Log.e(TAG, "failed to read persistent preference", e); 62 | pref.enabled = false; 63 | pref.minimumOverrideBacklightLevel = 0.0f; 64 | pref.duplicateApplicationWorkaround = false; 65 | } 66 | return pref; 67 | } 68 | 69 | public DisplayTransformManagerProxy getTransformManager() { 70 | if (dtm == null) { 71 | try { 72 | var localServiceClass = XposedHelpers.findClass("com.android.server.LocalServices", systemServerClassLoader); 73 | var displayTransformManagerClass = XposedHelpers.findClass("com.android.server.display.color.DisplayTransformManager", systemServerClassLoader); 74 | var dtmobj = XposedHelpers.callStaticMethod(localServiceClass, "getService", displayTransformManagerClass); 75 | dtm = new DisplayTransformManagerProxy(dtmobj); 76 | } catch (Exception e) { 77 | Log.e(TAG, "failed to obtain DisplayTransformManager", e); 78 | } 79 | } 80 | return dtm; 81 | } 82 | 83 | @SuppressLint("PrivateApi") 84 | private static int resolveGainLayer(ClassLoader classLoader) { 85 | try { 86 | var displayTransformManagerClass = classLoader.loadClass("com.android.server.display.color.DisplayTransformManager"); 87 | final var invertColorLayerField = displayTransformManagerClass.getDeclaredField("LEVEL_COLOR_MATRIX_INVERT_COLOR"); 88 | return invertColorLayerField.getInt(null); 89 | } catch (ReflectiveOperationException e) { 90 | return 299; 91 | } 92 | } 93 | 94 | public void notifyAllListeners() { 95 | final var lastRequest = getLastBacklightRequest(); 96 | final var lastOverrideState = getLastBacklightOverrideState(); 97 | final var lastOverride = lastOverrideState.overrideRequest; 98 | final var pref = getPreference(); 99 | Log.d(TAG, String.format(Locale.ROOT, "enable=%s, backlightLevel=%f->%f, backlightNits=%f->%f, transformGain=%f", 100 | pref.enabled, 101 | lastRequest.backlightLevel, 102 | lastOverride.backlightLevel, 103 | lastRequest.backlightNits, 104 | lastOverride.backlightNits, 105 | lastOverrideState.gain)); 106 | callbackExecutor.execute(() -> { 107 | synchronized (callbacks) { 108 | final var itr = callbacks.listIterator(); 109 | while(itr.hasNext()){ 110 | final var it = itr.next(); 111 | try { 112 | it.onBacklightUpdated(lastRequest, lastOverride, lastOverrideState.gain); 113 | } catch (RemoteException e) { 114 | // remove invalid remote 115 | itr.remove(); 116 | } 117 | } 118 | } 119 | }); 120 | } 121 | 122 | public void addListener(IBacklightOverrideStateListener listener) { 123 | synchronized (callbacks) { 124 | callbacks.add(listener); 125 | Log.d(TAG, String.format(Locale.ROOT, "addListener: %d listeners registered", callbacks.size())); 126 | } 127 | notifyAllListeners(); 128 | } 129 | 130 | public void removeListener(IBacklightOverrideStateListener listener) { 131 | final var targetBinder = listener.asBinder(); 132 | if (targetBinder == null) return; 133 | synchronized (callbacks) { 134 | callbacks.removeIf(it -> { 135 | final var itBinder = it.asBinder(); 136 | if (itBinder != null) { 137 | return targetBinder.equals(itBinder); 138 | } 139 | return Objects.equals(listener, it); 140 | }); 141 | Log.d(TAG, String.format(Locale.ROOT, "removeListener: %d listeners registered", callbacks.size())); 142 | } 143 | } 144 | 145 | private boolean validatePreference(BacklightOverridePreference pref) { 146 | if (pref.minimumOverrideBacklightLevel < 0.0f || pref.minimumOverrideBacklightLevel > 1.0f) 147 | return false; 148 | return true; 149 | } 150 | 151 | private void setPreference(BacklightOverridePreference pref) { 152 | if (!validatePreference(pref)) return; 153 | float newMinimumNits = 2.0f; 154 | if (displayDeviceConfig != null) { 155 | newMinimumNits = displayDeviceConfig.getNitsFromBacklight(pref.minimumOverrideBacklightLevel); 156 | } 157 | 158 | Log.d(TAG, String.format(Locale.ROOT, "setPreference: enabled=%s, minimumOverrideBacklightLevel=%f, minimumOverrideBacklightNits=%f", 159 | pref.enabled, pref.minimumOverrideBacklightLevel, newMinimumNits)); 160 | 161 | preference = new BacklightOverridePreferenceLocal(pref.enabled, pref.minimumOverrideBacklightLevel, newMinimumNits, pref.duplicateApplicationWorkaround); 162 | 163 | final var lastRequest = getLastBacklightRequest(); 164 | // skip refresh if setBacklight is not called yet 165 | if (Objects.equals(lastRequest, BacklightRequest.INVALID)) return; 166 | 167 | if (backlightAdapter != null) { 168 | try { 169 | backlightAdapter.setBacklight( 170 | lastRequest.sdrBacklightLevel, 171 | lastRequest.sdrBacklightNits, 172 | lastRequest.backlightLevel, 173 | lastRequest.backlightNits 174 | ); 175 | } catch (Exception e) { 176 | // ignore 177 | } 178 | } 179 | } 180 | 181 | public BacklightOverridePreferenceLocal getPreference() { 182 | return preference; 183 | } 184 | 185 | private BacklightOverridePreference getPreferenceForBinder() { 186 | var pref = getPreference(); 187 | var result = new BacklightOverridePreference(); 188 | result.enabled = pref.enabled; 189 | result.minimumOverrideBacklightLevel = pref.minimumOverrideBacklightLevel; 190 | result.duplicateApplicationWorkaround = pref.duplicateApplicationWorkaround; 191 | return result; 192 | } 193 | 194 | public BacklightRequest getLastBacklightRequest() { 195 | return lastBacklightRequest; 196 | } 197 | 198 | private void setLastBacklightRequest(BacklightRequest request) { 199 | lastBacklightRequest = request; 200 | } 201 | 202 | public void setLastBacklightOverride(BacklightOverrideState state) { 203 | lastBacklightOverride = state; 204 | } 205 | 206 | public BacklightOverrideState getLastBacklightOverrideState() { 207 | return lastBacklightOverride; 208 | } 209 | 210 | public BacklightOverrideState getOverrideBacklightAndGain(BacklightRequest request) { 211 | float overrideBacklight = request.backlightLevel; 212 | float overrideNits = request.backlightNits; 213 | float overrideSdrBacklight = request.sdrBacklightLevel; 214 | float overrideSdrNits = request.sdrBacklightNits; 215 | float gain = 1.0f; 216 | 217 | setLastBacklightRequest(request); 218 | 219 | var pref = getPreference(); 220 | 221 | if (pref.enabled) { 222 | final var minOverrideBacklight = pref.minimumOverrideBacklightLevel; 223 | final var minOverrideNits = pref.minimumOverrideBacklightNits; 224 | if (request.backlightLevel < minOverrideBacklight) { 225 | overrideBacklight = minOverrideBacklight; 226 | overrideNits = minOverrideNits; 227 | gain = (request.backlightNits / minOverrideNits); 228 | gain = Math.max(gain, deviceMinimumBacklightNits / minOverrideNits); 229 | //gain = Math.max(gain, overrideService.minimumGain); 230 | //gain = Math.max(gain, 0.05f); 231 | } 232 | 233 | // also override sdr brightness if they don't differ much perceptually 234 | if (Math.abs(PerceptualQuantizer.NitsToSignal(request.sdrBacklightNits) - PerceptualQuantizer.NitsToSignal(request.backlightNits)) < 1.0/512.0) { 235 | overrideSdrBacklight = overrideBacklight; 236 | overrideSdrNits = overrideNits; 237 | } 238 | } 239 | var override = new BacklightRequest( 240 | overrideSdrBacklight, 241 | overrideSdrNits, 242 | overrideBacklight, 243 | overrideNits 244 | ); 245 | var result = new BacklightOverrideState(override, gain, pref); 246 | setLastBacklightOverride(result); 247 | return result; 248 | } 249 | 250 | public void setTransformGain(float gain) { 251 | if (getPreference().duplicateApplicationWorkaround) { 252 | gain = (float)Math.sqrt(gain); 253 | } 254 | final var dtm = getTransformManager(); 255 | if (dtm != null) { 256 | // if (!dtm.needsLinearColorMatrix()) { 257 | // TODO: gamma gain 258 | // } 259 | final float[] mtx = { 260 | gain, 0f, 0f, 0f, 261 | 0f, gain, 0f, 0f, 262 | 0f, 0f, gain, 0f, 263 | 0f, 0f, 0f, 1f 264 | }; 265 | dtm.setColorMatrix(gainLayer, mtx); 266 | } 267 | } 268 | 269 | private final class BinderService extends IBacklightOverrideService.Stub { 270 | @Override 271 | public BacklightOverridePreference getPreference() throws RemoteException { 272 | return getPreferenceForBinder(); 273 | } 274 | 275 | @Override 276 | public void putPreference(BacklightOverridePreference pref) throws RemoteException { 277 | setPreference(pref); 278 | } 279 | 280 | @Override 281 | public void registerBacklightOverrideStateListener(IBacklightOverrideStateListener listener) throws RemoteException { 282 | addListener(listener); 283 | } 284 | 285 | @Override 286 | public void removeBacklightOverrideStateListener(IBacklightOverrideStateListener listener) throws RemoteException { 287 | removeListener(listener); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------