Reverse Engineering Android Pie’s Logarithmic Brightness Curve

TJ
5 min readNov 21, 2018

The Android Developer Blog recently put out a post detailing the strides forward they’ve taken to make adaptive brightness better. It’s an informative post and it highlights benefits to users that are immediately impactful and quite welcome.

What does it mean for developers though?

How Android’s Brightness Works

To change the brightness of an Android device’s screen you first need the user’s permission to write to settings. The permission is obtained by redirecting the user to the settings app via the following intent from your Activity or Context holder:

startActivity(new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, Uri.parse("package:" + this.getPackageName())));

You pass in your app’s package name because you’re requesting permissions for your app, and your app only from the system’s settings. Once the user has granted permission, you can now change the System brightness with your app.

Now brightness in Android is a tad peculiar. Since brightness is a System setting, the API to change it is the following:

Settings.System.putInt(contentResolver, Settings.SCREEN_BRIGHTNESS, byteValue);

The first argument in that method is a Content Resolver, usually gotten from a Context instance via Context.getContentResolver() call, but the second argument is the more interesting of the two. It’s a String URI locating the system setting we wish to adjust. In our case it’s the screen brightness, and we’re passing it a value in the range 0–255, hence the name byteValue. The values differ for each system setting, and you’ll have to do a fair bit of prodding, usually around StackOverflow to find the URI for the system setting you’re interested in.

It all seems easy enough, but what happens if adaptive brightness is on? Well, nothing. That’s because the system value for adaptive brightness is under another URI. Painful still, this URI is private. A couple of enterprising folks on StackOverflow have deduced this URI, and all well was good till Google shut that door in API level 24. If you try that workaround now, your app will crash with a SecurityException.

That’s a bit of a pickle isn’t it? Therefore, to manually control brightness from your app, you have to request adaptive brightness be turned off first using the following:

Settings.System.putInt(contentResolver, Settings.SCREEN_BRIGHTNESS_MODE, Settings.SCREEN_BRIGHTNESS_MODE_MANUAL);

This is inconvenient for users as sometimes they just want to make quick adjustments and be on their way, so the task is on you the developer to come up with whatever heuristic to turn adaptive brightness on again at a more opportune time. In my case, I elect to turn it back on when the screen goes off and it’s turned again, keeping the manually adjusted value for the entire session the screen is on.

And then comes Android P

So what did Android P do differently? Well as the Google Developer’s Blog post outlined, quite a bit. Brightness is no longer linear, so there isn’t a 1 : 1 mapping of brightness percentages to the byte values anymore. Whereas 25, 50, 75 and 100 percent mapped to 64, 128, 192 and 256 for brightness before, they do not anymore. That’s a bit of a conundrum for a developer like me, user’s have paid for this app, and it not working on the latest and greatest version of Android isn’t acceptable. So I had to roll up my sleeves and go digging.

First thing I did was try to understand what had gone wrong. The Android Developer blog post didn’t exist back then so the best clue I got was from a Reddit comment from a developer from XDA Developers. That comment is amazing, not only did it explain what was going on, but it explained some implementation flaws which was crucial for me to know to successfully reverse engineer, so thanks a bunch u/defet_.

So I just have to figure out what logarithmic curve the new brightness follows, and just apply that on Android Pie. Sounds easy enough. I just need the byte value of brightness in the content resolver at different percentages, and I’ll be on my merry way. Now there’s no API for this conversion, so I had to painstakingly adjust the brightness on my phone with break points in the Android Studio debugger, and read the percentage brightness off the phone, no bueno.

So with that fairly arduous process out of the way, I got the following logarithmic curve:

A fun reminder of undergraduate engineering lab

That looks like we’ve got a pretty good fit! So mapping from brightness values to user understandable percentages and vice versa should be a breeze, right?

Unfortunately no, because a curve fit is well, a curve fit. Converting a byte value to percentage and then back to a byte value would give you the same value on paper, correct to however significant figures you calculated to with no issues. However, the content resolver stores the value as an int, so after casting, the value you calculate, combined with int casting and errors in the curve fit would cause some screen flickering, especially at the lower brightness values. At this point, I gave up trying to be too clever and went with a solution that I knew would work: a lookup table.

I already had the x and y values, I just needed an efficient way to find a certain x, given a certain y and vice versa. Sounds like a job for binary search!

The implementation of the binary search is given below. There are some implementation details to note:

  1. It’s trivial to go from a percentage to a byte value, the entire domain is mapped with no repetitions. It’s however non trivial to go from a byte to a percentage; there is repetition in the lookup table, especially at the higher ranges. Also, you are not guaranteed to have the byte value mapped. At that point, I just have to crawl to the closest value that matches my query.
  2. Since this is a binary search, looking for a value should take O(logN) time, so given around a 100 values, a look up should average around six passes.
  3. If the value looked up is a byte without direct mapping, a crawl would have to take place. Crawling is is O(N) time, but a crawl should never exceed a 1 or 2 values at most after the binary search, so we’re still decently efficient.
  4. At the lowest brightness values, it’s better to just crawl. Otherwise, flickering would occur as there’s not much to choose between 6 and 11 in the byte value domain when binary searching, however the difference is very easily noticed by eyes in terms of screen flicker.
  5. In the crawl method implementation, I really tried to make the for loops with if conditions fit in one line just for the sheer pleasure of it. This is a personal app with one maintainer, so I can get away with it, but I’d face PR hell if I tried it at work. It was done as a form of personal code golf, please don’t berate me for it. As a bonus, It’ll mildly annoy my buddy Adam.

There you have it! It was a fun experience for me reverse engineering this, but I really do wish Android provided an API. If you’d like to check the app out, you can find it on Google Play here:

--

--