├── 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 |
15 |
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 |
--------------------------------------------------------------------------------