The data this post is about is live on my battery page, updated whenever I pull a fresh copy off the phone.

On 4 September 2022 I sent AccuBattery support a one paragraph feature request:

Please add 'Export history as xls/csv' for charging/discharging history. (Atleast in 'pro' version)

I want to see my 6 month or 1 years history on my PC/excel. (For Some research)

(I am pro user.)

I followed up on 8 February 2024 with the same text, because nothing had changed. Four days later Chad from support replied, and it was a genuinely helpful answer:

Thank you for your email. AccuBattery doesn't have any backup/export option at the moment as it's still on the to-do list. (If your phone is rooted you can access the sqlite database in our app folder and already get all the data though: /data/data/com.digibites.accubattery/databases/batterysaver.)

Backup functionality is going to be the next major feature we add but it also ties into this larger web interface project we are planning to introduce afterwards and at this point it's not clear yet how we will decide to structure the releases and what can be expected when.

That was two and a half years ago. There is still no export button. I stopped waiting and took the path Chad pointed at.

I am a paying Pro user, and this is my own battery data on my own phone. That is worth saying out loud, because everything below is just reading a file I already own.

First, the ways that do not work

I tried the unprivileged routes first, on the assumption that root should be a last resort. All three failed, and they failed in ways worth documenting because most guides online still recommend them.

run-as lets you become an app's own UID, but only for a debuggable build:

$ adb shell run-as com.digibites.accubattery ls databases
run-as: package not debuggable: com.digibites.accubattery

A Play Store release is never debuggable. Dead end.

adb backup is the classic answer, and it is deprecated but still present. Before spending a confirmation tap on it, check whether the app even permits it:

adb shell dumpsys package com.digibites.accubattery | grep pkgFlags
pkgFlags=[ HAS_CODE ALLOW_CLEAR_USER_DATA ]

No ALLOW_BACKUP in that list, which means android:allowBackup="false" in the manifest. I ran it anyway to be sure:

$ adb backup -f accu.ab -noapk com.digibites.accubattery
WARNING: adb backup is deprecated and may be removed in a future release
Now unlock your device and confirm the backup operation...

47 bytes came back:

00000000: 414e 4452 4f49 4420 4241 434b 5550 0a35  ANDROID BACKUP.5
00000010: 0a31 0a6e 6f6e 650a 78da 6260 1805 a360  .1.none.x.b`...`

A header, then an empty zlib stream. The backup ran and contained nothing. On Android 15 this path is doubly dead anyway, since Google has been stripping app data out of adb backup since Android 12.

Reading the path directly, predictably:

$ adb shell ls /data/data/com.digibites.accubattery/databases
ls: /data/data/com.digibites.accubattery/databases: Permission denied

The part that made it easy

My phone runs Evolution X rather than the stock ROM, and that changes the whole problem. Custom ROMs inherit LineageOS's rooted debugging toggle in developer options. Flip it and adb root restarts adbd as root, with no Magisk, no patched boot image, and most importantly no wipe:

$ adb root
restarting adbd as root
$ adb shell whoami
root

This is the detail people miss when they conclude "unrooted, so impossible". You do not need a rooted system to read an app's data directory. You need a root adb daemon, and every custom ROM I know of ships that as a checkbox. If you are on stock and locked, it really is impossible without unlocking the bootloader, and unlocking wipes userdata, which destroys the exact history you were trying to rescue. That trade is self-defeating. Do not take it.

With root adb, the pull is boring, which is the goal:

adb root
adb shell am force-stop com.digibites.accubattery
adb pull /data/data/com.digibites.accubattery/databases/batterysaver

The force-stop matters. Check the journal mode before trusting a single-file copy:

-rw-rw---- 1 u0_a616 u0_a616 89858048 batterysaver
-rw------- 1 u0_a616 u0_a616        0 batterysaver-journal

A zero byte -journal and no -wal means rollback journaling with nothing outstanding, so the one file is the whole database. If you see a -wal file instead, copy that and -shm too, or you will silently lose the most recent writes.

Mine came out at 89.8 MB and clean:

$ sqlite3 batterysaver "PRAGMA integrity_check;"
ok

What is actually in there

Eight tables, and only three of them matter:

powercycleinfo        863       every charge and discharge session
timeseries         231780       the fine grained graph data
appusageevent2     1016345      app foreground/background events
stringpool           2247       interned package and class names
deviceconfig            1       calibration constants

powercycleinfo is the prize, and it is refreshingly honest: one row per session, with the payload as plain JSON.

CREATE TABLE powercycleinfo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  time INTEGER UNIQUE NOT NULL,
  charging TINYINT NOT NULL,
  json TEXT NOT NULL,
  duration INTEGER NOT NULL DEFAULT 0,
  sync_id INTEGER
)

One discharge row, formatted:

{
  "charging": false,
  "counters": {
    "ON":  { "batteryPercentageChange": -73.0, "elapsedMillis": 21350938, "milliAmpSeconds": -7389954.43 },
    "OFF": { "batteryPercentageChange": -24.0, "elapsedMillis": 10555311, "milliAmpSeconds":  -883170.57 }
  },
  "durationMillis": 191426286,
  "startPercentage": 100.0,
  "lastPercentage": 3.0,
  "powerSource": "BATTERY",
  "startEpochMilli": 1761843033533,
  "to100PercentMilliAmpHours": -2278.1085890269524
}

ON and OFF are screen on and screen off, split out separately, which is the number AccuBattery's UI is built around. Everything the app shows you about drain rates comes from those two counters.

appusageevent2 stores package and class names as integer IDs into stringpool, and the app ships a view that joins them back for you:

SELECT timestamp, package, class, foreground FROM appusageevent2view ORDER BY timestamp;

Pairing each foreground=1 with its next foreground=0 gives real foreground sessions. Mine came to 507,620 sessions across 274 packages.

The field that lies to you

to100PercentMilliAmpHours sounds like it has already been extrapolated to a full 0 to 100% swing. It has not. It is the raw charge moved during that session.

I averaged it across sessions on my first pass and got a meaningless number, because a 25% discharge and a 99% discharge were being averaged as if comparable. Dividing by the actual depth is what you want:

date         depth   raw mAh   implied full
2025-07-18     25%       686           2745
2025-07-19     81%      2430           2999
2025-07-21     99%      2684           2711
2026-09-02     97%      2761           2846
2026-09-06     45%      1080           2399

Shallow sessions extrapolate badly, so I only keep sessions of 40% or deeper. Across 345 deep discharges that gave me 2654 mAh, down from 2878 a year earlier, and I nearly published that as the answer.

Then I looked at the app

AccuBattery on the phone was showing 3187 mAh. My number was 500 mAh lower, which is far too big a gap to be rounding.

The mistake was that I had only looked at discharges. Running the same calculation over charge sessions gives a completely different answer:

                    charging   discharging
Jul 2025                3446          2940
Sep 2026                3195          2579

Both columns are the same cell measured over the same months. Charging says 3195 mAh, discharging says 2579 mAh, and neither is a mistake. Charge counted going in genuinely exceeds charge counted coming back out, partly because charging is lossy and partly because the kernel's current sensor is far better behaved at a steady charging current than across the ragged draw of actual use.

AccuBattery's headline capacity comes from the charging side. That is why the app tells you to charge to 100% to take a reading, and I had been ignoring exactly the half of the data it uses. The charging figure lands within 1% of what the app displays.

One more detail separates a close answer from a matching one. Summing the milliAmpSeconds across the counters gives 3195 mAh, while to100PercentMilliAmpHours on the same sessions gives 3049. The app tracks the counter sum, so that is the field to use.

Corrected, over fourteen months:

first 20 charge sessions   3435 mAh
last 20 charge sessions    3212 mAh

About 6.5% lost, and 75.6% of the 4250 mAh this cell is rated for. That is the answer I wanted in 2022.

The lesson I actually learned here is the one I nearly skipped. I had already written "verify the absolute against what the app's own UI tells you" as a throwaway caveat, then almost shipped without doing it. The app was sitting in my pocket the whole time with the right number on screen. When you reimplement someone else's calculation, check your output against theirs before you trust a word of it.

The one part I did not crack

timeseries holds the fine grained data behind the charts: batteryPercent, current, voltage, temperature, screenState and networkTransport, each at 38,630 rows covering fifteen minute windows.

The bytes column starts with 78DA, so it is zlib, and inflating is trivial. What is inside is not JSON:

64000000 b014c814 04000000 7f000000 6869d70b 140014001400140014001400...

A 16 byte header carrying what looks like a magic number, a unit tag and a sample count, then packed values. I can read the count and I can see the shape, but I have not worked out the delta encoding, and I did not need to. Session level data answers every question I originally had. If you want to attack it, powercycleinfo gives you a strong oracle: you already know the true start and end percentage of every window, so you can check any decoding against known values.

Notes if you do this yourself

  • Check pkgFlags before spending time on adb backup. No ALLOW_BACKUP means the answer is no, whatever the tutorials say.
  • adb root is not the same as a rooted phone. Developer options, rooted debugging. On Evolution X and most LineageOS derivatives it is right there.
  • Do not unlock a locked bootloader to rescue history. The wipe destroys what you came for.
  • Force-stop first, and look for -wal. A live database copied under load can be inconsistent.
  • Copy the database, work on the copy. Never point a script at the phone's live file.
  • Read the field names sceptically. to100PercentMilliAmpHours cost me one wrong conclusion before I checked it against session depth.

My history only reaches back to July 2025, because that is when I last flashed this ROM and AccuBattery's data went with the wipe. Fourteen months instead of the four years I have been asking for. Which is, in the end, the actual argument for shipping an export button: the data is only ever one flash away from being gone, and a .csv in my Downloads folder would have survived.

The database schema is stable and the JSON is readable. If you are a Pro user sitting on years of history, it is a twenty minute job to get it out. I would still rather press a button.