An Android gaming overlay, performance monitor and per-game profile manager — built on the rule that every number it shows is one Android actually reported.
Android 8.0 (API 26) or newer · signed release build · sideload, no store listing · no account, no backend, nothing you record leaves the device
The floating dock's buttons become yours to arrange, the active profile can be put back one field at a time without ending play, and an in-game trigger can be placed by touch — plus the fix for a release-blocking bug where the trigger-placement overlay swallowed every touch except the game's own pill.
- Dock customisation. Rearrange the floating dock's buttons and choose which of them appear, saved per game — so the dock a game raises holds exactly the controls that game needs.
- Quick actions on the dock. Put shortcuts to GameCore's existing controls onto the dock for one-tap access without opening the full panel; a quick action runs through the same handler the panel's own tile does and invents no new capability.
- Revert settings mid-session. A card on Home lists exactly what the active profile changed on your device and puts any of it back without ending play — the same reading-paired restore a profile does on exit, offered one field at a time while the game runs.
- In-game point-trigger placement. Place a volume-key tap target by touching the spot on screen, from inside the game and without opening the app. It is saved per game, and the tap it later fires is the one synthetic input the app can send (see below), gated on Shizuku.
A second overlay surface and four stand-alone tools, each holding the app's one rule: what it cannot measure or do, it says plainly rather than faking.
- Floating Dock. A small draggable dock over your game with its own quick control panel; it snaps to the screen edge, is turned on from Settings, and sits alongside the existing floating button rather than replacing it.
- Screen Extraction. Capture the current screen, crop a region, then save or share the result — drawn and cropped on the device and handed out through the same one-shot share grant every other artefact uses.
- Touch Sampling Monitor. A live read of the device's real touch input rate. Where the hardware will not report it, it reads Unavailable rather than a plausible number — the rule every reading in the app follows.
- Volume Button Point Trigger. Map a volume key to tap a saved screen point for a game — single, double or hold. It needs the optional accessibility service, so the key reaches GameCore from inside another app, and Shizuku, so the tap can be injected; without either, the control says so rather than pretending to work.
- What's New. Shows the features a release introduced the first time you open the app after updating, and lets you browse every past update any time from Settings.
Three leaf screens, each turned on or off from Settings, and each holding the app's one rule: a figure it cannot measure is shown as "Unavailable", with the reason, never as a zero.
- Network Stability Mode. A live read of the connection you play on — transport, signal, estimated link speed, device-wide download and upload, and whether the system calls it metered — with a stability window built from TCP-handshake bursts. It reports ping (median handshake time) and jitter (the variation between consecutive probes, the honest stand-in for the packet-loss figure it will not fake), and how many probes in the window completed. Where Android allows it, it also keeps the device from switching networks mid-game.
- Advanced Performance HUD. A customisable heads-up display over FPS, CPU, GPU where the device exposes a load node, RAM, temperature, battery, ping and session time, as a compact line or an expanded card. Any stat the device will not report reads "Unavailable" rather than a made-up value.
- Custom Overlay Modules. Turn individual overlay modules on and off, drag them to position, resize the ones that support it, and choose exactly which information each shows — saved as an overlay layout per game that a profile can raise by name.
The feature people ask for by name — keep the last few seconds of play, and decide to save them after they have already happened.
- Instant Replay. A rolling video buffer that always holds the most recent stretch of the game — 15, 30, 60 or 120 seconds, 30 by default — so a moment you did not know you would want is already recorded by the time you reach for it. When you save, the buffered window is written out as a single clip and nothing outside it is; the buffer itself lives in the app's own private cache and is trimmed back to the length you chose as it rolls, so it never grows without bound. It is off until you turn it on, set per game like every other profile field, and the first time you enable it a one-time notice states the three things worth knowing first: the buffer records the whole time the game is open, everything stays on this device, and this version is video only — it holds no microphone permission and captures no audio, on purpose, because a "record audio" switch that produced a silent track on half the games it was used with is exactly the kind of control this app refuses to ship. It reuses the same screen-capture consent the recorder already asks for, so it adds no new permission. An in-game chip shows when it is armed, and a save control sits in the overlay while the game runs.
Two features about reaching past the settings a game exposes — to the files behind them, and to the resolution it renders at.
- A config editor. Browse a game's own configuration files and edit one in place, with the whole change shown as a line-by-line diff before a single byte is written — nothing is saved until you confirm exactly what changed. The write is whole-file and atomic, so a config is never left half-applied. The first time you edit a game's config its original is backed up automatically; you can keep named checkpoints and restore any of them later, and if the game has been updated since that backup the editor says so, because an edit made against a config the update already replaced is an edit against a stale file. It reaches those files through the same elevated shell the rest of the app uses, so it needs Shizuku — without it the screen says so and points at the setup — and a path outside the selected game's own config is refused rather than opened. A one-time notice explains the feature before the first edit, and can be shown again.
- Resolution override. A per-game render resolution, set as a scale of the display's native
size and applied through
wm sizewhen the game comes to the front — then cleared and the display put back exactly as it was when the game leaves, the same restore rule every other profile field follows. Lowering it can lift frame rate on a GPU-bound game at the cost of sharpness; it is per game, off unless you set it, and it asks once for confirmation the first time because it reshapes the whole display while the game runs. An in-game status chip shows when an override is active, so a changed resolution is never a mystery. Needs Shizuku, like every display-size write.
Also since 3.5, and until now in no release:
- A pinned magnifier. A loupe overlay that shows the centre of the screen enlarged — a crop-and-scale of a single capture frame, parked in a corner with its geometry chosen so it can never fall over the region it magnifies and mirror itself. There is no detection, no tracking and no aim logic: it enlarges pixels and nothing else. It uses the same screen-capture permission the other overlays do.
- Panel macros. A named, ordered set of overlay actions you already have, fired in sequence by one tap. A macro invents no new capability — every step runs through the same handler the panel's own tiles use — so all it adds is the order and the count. They live as an editable, reorderable config list, not a database table.
- Config backup and restore. Export every configuration store — profiles, presets, overlay layouts, macros — to one file you can keep or move to another install, and restore it later. It touches configuration only: your recorded sessions and Aim Lab runs are never written into the file and a restore can never overwrite them, and everything a restored file carries is validated before it reaches a store.
- Profile transfer. Share one or more game profiles as a single file, presets and all, and import them elsewhere — recreating what is missing and matching-then-remapping what already exists, so an import never silently duplicates a preset or clobbers one you had.
- Profile suggestions. A starting profile derived only from what a game was actually measured doing, with a plain sentence behind every field it fills. A dimension the sessions did not measure enough to speak to is left alone; a game with thin or contradictory history gets no suggestion at all rather than a guess dressed as advice. It only drafts — nothing is saved or applied until you open it in the editor.
- Per-sample export. Alongside the one-row-per-session export, write one row per sample — the raw time series a graph is drawn from — as CSV or JSON. It keeps the same honesty rule the aggregate export follows: blank, never zero, for a reading a sample never took.
Four features about the app setting itself up, and then looking after the session on its own.
- A first-run setup wizard, and a Setup health screen. A new install now walks through what GameCore needs — which features you want, and only the permissions those features actually use — instead of leaving you on an empty Home screen to work it out by trial. Nothing is granted on your behalf and no step is required: skip all of it and the app still runs, minus whatever you skipped. Afterwards, Settings › Setup health lists every item with its real state — Ready, Not set up, or Unavailable with the reason Android gave — a Fix button that opens the exact system page, and a re-check when you come back from it. Upgrading never re-runs the wizard over your existing profiles; it offers a dismissible Home card instead, and only when something genuinely needs attention.
- Thermal auto-downshift. Off by default, per profile. When the device holds a temperature you choose, or the thermal status Android reports crosses the floor you set, the refresh rate steps down — and steps back up once it has stayed cool long enough. The hysteresis, the sustain window in each direction and the minimum time between changes are all yours to set, so it cannot sit there oscillating. The session records how many times it stepped down and the lowest rate it reached.
- Network check. Off by default, per profile. Before a launch it can warn you about the connection you are about to play on; during a session it can alert you when the connection turns poor — high latency, jitter, or probes that stop coming back at all. It still refuses to call any of that packet loss, because a TCP handshake that times out is not a dropped packet. It reads the transport, the Wi-Fi band and the link speed — never the network name, never the BSSID, and it asks for no location permission, which is what Android would demand before handing those over. Anything Android will not report shows as Unavailable with the reason.
- Keep full performance under battery saver. Off by default, per profile. Battery saver caps the refresh rate and the governor on most builds; with this on, a profile holds full performance for that game anyway, puts the saver's own behaviour back when the game exits, and notices — and records — when the system switched the saver back on behind it. No new access of any kind: it goes through the same elevated shell the refresh-rate setting already uses.
Also since 3.2, and until now in no release:
- A redesigned in-game overlay. The floating button has real states — dimmed when idle, an orange or red dot when the shared thermal classifier says hot or critical — three sizes and four corner snaps, and it keeps its place per orientation. Tapping it opens a quick sheet: a narrow panel on the screen edge nearest the button, holding up to six toggles you pin yourself, the brightness and volume sliders and the session clock, so the things you reach for mid-game no longer need the full panel and most of the game stays visible. The full panel is still there, behind More or an optional double tap, now divided into Display, Overlays, Capture and Session tabs with a hold-to-confirm End session (the older split-across-both-edges layout keeps its previous design). The stats pill comes as a compact single line or a detailed card, and it stops sampling the moment nothing is looking at it — including when the screen goes off, where it used to keep ticking.
- Themes. Follow system, Dark, Light or AMOLED black — a true
#000000background that switches OLED pixels off rather than dimming them — with eight accent colours or one you pick yourself, applied across the app and the overlay windows alike.
Two additions to the Aim Lab, both about the weapon and the way you hold the phone.
- Fire modes. A weapon now discharges in one of three ways — single, a fixed burst, or full auto — and the training loop honours it: a burst walks up the recoil pattern at the real fire-rate cadence rather than landing all at once, and the magazine and reload gate every mode the same. The weapon editor picks the mode and, for burst, the round count; a built-in Burst Carbine ships so the mode is there to try without building one. Older saved weapons read back as auto — exactly what they already did — so nothing changes under you.
- On-screen controls on every mode. The saved control layouts (HUDs) you build in the editor can now be drawn over the arena during a run from any mode's setup screen, not just some — pick one, or leave it on "None" for a clean view. The choice is per run and never forced.
Also from 3.1: the Aim Lab runs in landscape with a real horizontal field-of-view setting, the gyro axes remap correctly per screen rotation, and control layouts are stored per orientation.
An offline Aim Lab — a first-person 3D training arena, built inside GameCore and held to the same rule as everything else: nothing it reports is invented.
- A real first-person 3D room, not a flat target board. You stand inside an enclosed arena — checkered floor, grey walls and ceiling, light depth fog — with a low-poly weapon held at the bottom-right and glossy spheres floating at varied distances and angles. The crosshair is fixed dead centre; you look by dragging, and a shot is the ray straight out of the crosshair, hit-tested against the spheres in 3D. Rendered with OpenGL ES 2.0 and nothing else: no game engine, no third-party 3D library, no model or texture files — the room, the spheres and every weapon are built procedurally in code, and the shaders are plain source strings.
- Seven modes, all in the 3D view. Flick, Tracking, Reaction, Gyro, Recoil, Movement and a Free Practice sandbox. Aim error is measured in degrees, not pixels, so a result means the same on any screen. Recoil kicks the camera along the weapon's pattern and scores how well you pull it back; Movement walks you through the room and scatters shots taken on the move; Gyro turns the view from the real gyroscope, with no simulated motion if the sensor is absent.
- Your gear drives it. The weapon editor, sensitivity lab, control/HUD editor, statistics, personal records, session history and JSON/CSV export/import all carry over — and a weapon's fire rate, magazine, reload, spread and movement penalty are now actually felt in a run.
- Old training data is kept, and kept separate. Sessions recorded by the previous flat trainer stay in your history, labelled as legacy, and are never ranked against the new 3D scores, because the two are measured differently. The database upgrade is additive — no history is dropped.
- It stays out of the way when you leave. The 3D view renders only while a run is on screen; the moment you leave or background the app, rendering stops, the gyroscope is released and the run is cancelled. If a device cannot run OpenGL ES 2.0 the arena shows a plain "3D view unavailable" panel instead of crashing.
The Aim Lab is off by default behind a master switch, and turning it off is a real disable, not a hidden screen.
Five additions, all of them about reaching a setting without putting the game down.
- A second shape for the control panel. It can now split into two plates pinned to opposite screen edges — readouts on one, controls on the other, the game still visible between them, full screen height — instead of one plate below the button. Centred stays the default, because a panel you have already learned should not rearrange itself on an update. A Layout tile inside the panel switches between the two on one tap, so trying the other shape costs nothing and neither does going back.
- Refresh rate from the panel. Every rate this display reports, as chips under the tile grid, plus the way back off a pinned one. The tile lights only from a change GameCore made and confirmed: the platform is entitled to drop a pinned panel to 60 Hz on a static screen, so a low reading is not evidence the pin failed and a high one is not evidence it holds. A rate that was written but could not be read back says so rather than claiming it took.
- Crosshair quick-select. Hold the crosshair tile for the active crosshair's design and colour without opening a screen. It offers the built-in colours plus the ones you already mixed somewhere with more room — no HSV square, because that is a two-handed control and this window is open over a game.
- An eleventh crosshair design, and more colours. A box: a square outline, for framing a target rather than marking a point.
- CPU core affinity presets, marked experimental. A per-profile choice of which cores a game's main process may run on — performance cores only, or all cores minus one efficiency core. The app says what this is worth in the same words every time: it may reduce stutter by controlling which cores the game runs on, it does not make the device faster, and depending on the game it can do more harm than good. Needs Shizuku. The mask the process was found on is recorded before anything changes and written back when the game exits.
Three additions, and four reported bugs fixed.
- Game storage. A screen that measures what every game is holding in cache, and clears one game's shared-storage cache on a tap. It reads the size, deletes, waits, reads again, and reports the difference between the two readings rather than an exit code — so a clear that freed nothing says nothing was freed, instead of claiming a number it did not measure. It never touches a save, a login or a downloaded asset. Details.
- What the connection did. Every session now carries a latency log: probes that completed, probes that did not, the worst reply, jitter, the longest run of failures, and one of five verdicts with the figures behind it in a sentence. No packet-loss percentage anywhere, because a refused TCP handshake is not a dropped packet. Details.
- A card you can share. A finished session renders to a 1080×1260 image and goes to your own share sheet. A reading this device could not take is a dash on that card and never a zero, an interrupted recording says its length is a lower bound, and the sample count travels with the averages. Drawn on the device; nothing is uploaded. Details.
The bugs, all four found in use rather than in a test: a crosshair that drew the dot preset whatever you picked — and the HUD, which had the identical bug, because a layout you arranged is not "any layout"; a profile edited after leaving a game that did nothing until GameCore was restarted; the resolution card stuck on "loading" after quitting a game; and a Do Not Disturb popup that put DND back on after you had turned it off by hand. The last one is now impossible by construction: GameCore restores a device setting only when it made the write and nothing outside GameCore has changed it since.
A display colour screen, with the same values reachable from inside a game.
- Eleven values. Red, green and blue gain; gamma as one slider or three; saturation, contrast, hue rotation and a brightness offset. Every slider shows its number, and tapping the number opens a keypad for an exact entry — held to the field's own range, so a hue turns ±180° while a gain stops at ±100%, and an out-of-range entry is refused rather than quietly clamped to something you did not type.
- Presets you own. Seven ship — Vibrant, Warm, Cool, Night, Protanopia, Deuteranopia, Tritanopia — and they are ordinary saved rows, not a fixed menu: open one, move a slider, save it under a new name, rename it, delete it. There is no cap on how many you keep.
- Per game. A profile can carry a colour preset. It is applied when the game comes to the front and put back when it leaves, from the reading taken before the change — the same restore rule every other profile field follows.
- In the overlay panel. Saturation, contrast and hue sit directly under the existing volume and brightness sliders, a Colour tile joins the action grid, and a long press on it drops your saved presets in as chips. Everything applies as the slider moves, with no confirm step.
- In the session report. Which preset, and which values, were active while the session ran.
And the reason the feature is built the way it is: Android exposes no per-channel colour
matrix to an app. ColorDisplayManager's matrix and SurfaceControl.setDisplayColorTransform
are hidden platform API that neither WRITE_SECURE_SETTINGS nor a shell running as uid 2000 can
reach. So GameCore projects what you asked for onto the sinks that do exist on the device it is
running on — night-display warmth, the display colour mode, the daltonizer,
reduce-bright-colours, colour inversion — and the screen lists every write it would make, names
each value this device has no sink for, and says why. A gamma slider that moved and quietly did
nothing would have been easier to build, and would have been exactly the lie this project exists
to avoid.
Colour correction needs WRITE_SECURE_SETTINGS, which Android never grants an app on its own —
Shizuku or a wireless-ADB pairing is the only way to hold it. Without it the screen says so and
points at the setup, rather than presenting sliders that write nothing.
Most "game booster" apps are theatre. They show a RAM figure, animate a progress bar, kill some processes Android immediately restarts, and report success. GameCore does have a switch that closes background apps when a game starts — off by default, per game, and it tells you what it measured afterwards rather than what it hoped for, including when the measurement failed or came out negative. The difference is not the capability. It is that the number is a reading and not a flourish. Its central type is
sealed interface Observed<out T> {
data class Value<T>(val value: T, val source: DataSource, val precision: Precision)
data class Restricted(val reason: RestrictionReason, val unlockedBy: AccessLevel?, ...)
data class Failed(val detail: String, val cause: String?)
}A reading is either a value that names where it came from and how exact it is, or an absence that names its reason. There is no default, no fallback constant and no estimate dressed up as a measurement. The consequence is visible in the app: some cards say "Not available on this device" or "Requires Shizuku" instead of showing a number. That is the feature, not a gap.
No root. No code injection, no reading or writing another process's memory, no anti-cheat interference. No fabricated statistics. No claiming a system call worked when it did not. No background service that closes apps on a timer, and nothing closed without a profile asking for it — never your launcher, your keyboard, the game itself, anything holding a foreground service, or anything on your never-close list. No thermal limit override. No modification of protected system files. No "clean everything" button — the storage screen deletes one named directory belonging to one game you tapped, and never a save, a login or a downloaded asset. No packet-loss percentage, because a refused TCP handshake is not a dropped packet. And nothing you record is uploaded: the shareable session card is drawn on the device and handed to your own share sheet.
The one thing that does write a game's own files is the config editor, and it stays inside that
game's shared Android/data/<package>/files directory — the config file you opened and edited,
its original backed up before the first change, each write shown to you line by line and committed
whole or not at all. Never its code, never a save in the private storage a shell running as shell
could not reach anyway, never a path that a .. could climb out of, and never without Shizuku and
a one-time notice you accept.
The full set of shell commands the app can construct lives in one file
(core/shizuku/ShellCommand.kt) and a unit test enumerates every one of them and asserts
that cmd, su, setprop, force-stop, pm clear, pm trim-caches, install and
friends are unreachable at any privilege level. Most of what that file can build reads or
writes something about this device — a settings key, the display size, GameCore's own
permissions. A few reach further, and each is bounded to a form the test pins literally so
that a second use cannot be added without it failing: am kill --user current <package>
closes one game's background processes; rm -rf /storage/emulated/<user>/Android/data/<package>/cache
clears one game's shared cache; taskset -ap <mask> <pid> pins one game's process to a set of
cores; input tap and input swipe send one synthetic tap or press-and-hold to a per-game
screen point the user placed, for the volume-button trigger — no text, no key event, and no other
input subcommand is expressible; the charge-bypass toggle writes a single digit to one validated
/sys/class/power_supply/<supply>/<node> — the one command in the app that runs sh -c, and it
passes the value (0 or 1) and the path as positional arguments ($1, $2) rather than splicing
either into the script text, after a test -w probe and a cat read of that same node; the GPU HUD
reads one validated GPU-load node under /sys with cat; and the config editor's ls, stat,
cp, mv and rm -f read and rewrite files inside one game's own Android/data/<package>/files —
never its cache, never its obb, never the private storage a shell-uid process cannot reach, and
never through a path a .. could climb out of. Every argument that originates outside the app's own
code is validated against a form before a command exists; every command but that one charge write
reaches exec as an argument vector; and that write runs a fixed script whose only inputs are its
two positional arguments, so there is nowhere a glob is expanded or a word is split.
Follow system, Dark, Light or AMOLED black, with a fixed palette of eight accents or a custom colour of your own. The choice applies to the whole app and to the overlay windows the service draws over your game, so the two never disagree.
- A draggable gaming button that snaps to any of four corners — or to the nearest edge on a free drag — remembers its place separately for portrait and landscape, survives rotation, and says what it knows without being opened: dimmed after a few seconds idle, with a small orange or red dot when the device is hot or critical.
- Tapping it opens a quick sheet on the screen edge nearest the button: up to six toggles you pin yourself, brightness and media volume, the game's own icon and the session clock. It is narrow on purpose, so the game stays visible beside it.
- Behind More — or an optional double tap on the button — is the full panel, in Display, Overlays, Capture and Session tabs: brightness and media-volume sliders, saturation, contrast and hue, screenshot, screen recording, Do Not Disturb, orientation lock, flashlight, colour presets, display shape, refresh rate, a shortcut back into the game, and a hold-to-confirm End session. Switching the panel's layout to split-across-both-edges keeps the earlier design, with the game visible between the two halves.
- A configurable performance pill, as a compact single line or a detailed card: pick which stats it shows and set its position, size, opacity, corner radius, text size and update interval. It samples only while something is looking at it — and not at all once the screen is off.
- A crosshair overlay with eleven designs — cross, dot, ring, ring-and-dot, cross-in-ring, T, X, chevron, corner brackets, box, or a PNG you import — and independent control of size, thickness, centre gap, rotation, opacity, colour and screen position. The drawn designs use Compose primitives, so they stay crisp at any size and ship no bitmaps.
- A visual HUD builder: drag widgets onto a live preview, choose from 18 stats, set each widget's text size, opacity, colour, label and background, and save layouts that a game profile can raise by name.
- A separate, lighter floating dock: a small draggable strip that snaps to a screen edge and carries one-tap shortcuts to controls you already have. Which buttons it shows, and their order, is yours to set and is saved per game, alongside the full panel rather than in place of it.
One profile per package, holding target refresh rate, brightness, display size, orientation lock, screen timeout, media volume, Do Not Disturb, which overlays to raise, a HUD layout, a crosshair preset, a colour preset, a performance mode, whether to free memory on launch, and whether to track the session — plus the three 3.5 smart features: thermal auto-downshift, the network check, and holding full performance under battery saver. All three are off unless you turn them on for that game.
Every adjustable field is nullable, and null means leave it alone — not "use a default". A profile that sets only brightness records what brightness was, changes it, and puts exactly that back when the game exits. Nothing else is touched, so nothing else can be restored to a value the device never had.
Automatic detection applies a profile when its game comes to the foreground and unwinds it when the game leaves. You can also launch a game straight from its profile screen.
Free RAM on launch is the one setting whose effect lands on your other apps, so it is off
by default, per game, and it runs last — after the overlay is up and the profile is applied,
because those are the things you are waiting for. GameCore never closes itself, the game, your
launcher, your keyboard, anything holding a foreground service (music, a recording, a
download, a navigation route), any part of the system, or anything on your never-close list in
Settings. With Shizuku it enumerates what is actually running, closes through the elevated
shell and re-reads the list to confirm; without it, it asks Android through
killBackgroundProcesses(), which reports nothing about what it did — so the summary says
"asked Android to close" rather than claiming a count it cannot verify. The freed figure is
two ActivityManager readings with the pass between them, and it is allowed to say it could
not measure, or that available memory did not rise.
Per-core CPU usage and frequency from /proc/stat and sysfs, memory from /proc/meminfo
and ActivityManager, battery level, charging source, health, temperature, voltage and
instantaneous current, thermal status, GPU load where the device exposes a /sys node for it,
display mode and rotation, the touch sampling rate where the device will report it, network type, latency and
throughput, and free storage — with live graphs, a configurable sampling interval, and
sampling that stops the moment nothing is looking at it.
Battery drain is reported as percent per hour rather than raw points lost, and only after five minutes of discharging, so a two-minute session cannot extrapolate to a headline figure.
Every tracked session is written to an encrypted Room database: the game, start and end time, duration, battery delta, average and peak CPU load, average and peak memory use, average and peak temperature, the refresh rate it ran at, the network it used, what its latency probes did, and the colour preset and values the display was held at. A session recorded before 1.1 reads back as no colour reading rather than as a neutral one — an absence, not an invented zero.
The session report draws those as graphs. The history screen filters, sorts, deletes and aggregates them. A session interrupted by the app's process being killed is repaired on the next launch and marked as ended by process death, rather than being silently dropped or left running forever.
What the connection did, across the whole session. The latency probe answers "how is it right now" from a burst of handshakes. A tracked session also keeps what happened across all of them: how many completed, how many did not, how many came back at least twice the session's own running average and at least 40 ms above it, the worst reading, the average spread between consecutive probes, and the longest unbroken run of failures. The report turns that into one word — Stable, Spiky, Unreliable, No reply, Not measured — and one sentence naming the figures behind it. The two are not symmetric on purpose: "stable" is withheld until at least three probes have gone out, while a single handshake that did not complete is reported at once. Two quiet probes are not evidence of a good connection; one refused handshake is evidence of a bad moment. A spike is measured against the session's own average rather than a fixed threshold, because 180 ms is an event on a connection that has been sitting at 30 ms and unremarkable on one that has been sitting at 190 ms. None of it is called packet loss.
A card you can share. The report has a share action that draws a 1080×1260 PNG on the
device — game, date, duration, average frame rate, average heat, battery drain — and hands it
to the standard share sheet as a one-shot read grant through the app's own non-exported
FileProvider, never as a file path. The card keeps the rules of the screen it came from,
with less room to explain them: a reading the device could not take is an em dash and never a
zero, an interrupted session's duration is marked as a lower bound, a per-hour battery figure
is only printed when the session was long enough to support one (below that the tile becomes
the points actually lost, which is a subtraction of two real readings), and the sample count
travels with the averages — four samples and four hundred are different claims, and a card is
read alone in a chat window with nothing beside it to qualify it. The game's own label is the
one string on it GameCore did not write, so it is stripped of control characters, collapsed
and clamped before being drawn, and it contributes nothing to the filename.
What each of your games is holding in cache, and the one part of it GameCore is willing to
delete. Every cache cleaner on a store shows one number and one button; the number is usually
the whole data directory and the button usually runs pm clear, which is why the reviews of
those apps are full of people who lost a save. So each game gets three figures instead of one:
- Cache — what the platform reports for the package, the same number as that app's own storage page in Settings.
- Clearable — the part of it GameCore can actually reach: the shared-storage cache directory, and nothing else. A separate reading rather than a fraction of the first, because Android 11 and below will not break the total down, and guessing the split would be guessing how much a button is about to free.
- Untouched — the rest. Saves, logins, settings, and the assets a game downloaded after install. Nothing GameCore does touches it, and it is on screen so that "freed 900 MB" can never be read as having come out of it.
The list is your games — an app that declares itself one, or an app you made a profile for, which is better evidence than the manifest — ordered largest cache first, with rows whose size could not be read placed last rather than treated as empty.
A tap reads the cache figure, deletes Android/data/<package>/cache, waits for the platform's
accounting to catch up, and reads the figure again. What it reports is the difference between
those two readings: not the size that was there beforehand, and not the exit code of the
delete, since rm -rf on an empty directory and on two gigabytes exit identically. The figure
is allowed to come out at nothing — the reachable part may already have been empty, or the game
may have refilled it in the second it took to look, and both are ordinary.
The copy of the cache inside the game's private data directory is out of reach: a shell running as the shell user cannot open another app's data directory, and GameCore holds no root path to one. That part is reported as remaining, next to a button for Android's own storage page for the app, which needs nothing granted and reaches more than this does. Measuring needs usage access, deleting needs Shizuku, and when either is missing the screen says which rather than offering an action that would achieve nothing.
Only rates the panel actually advertises are offered — the list comes from
Display.getSupportedModes(), not from a hardcoded 60/90/120.
A change is reported as applied only after it has been read back and confirmed. This
matters for a specific, verified platform behaviour: on several MediaTek and PowerVR
devices the standard preferredDisplayModeId / Surface.setFrameRate() calls are accepted
without error and then ignored. An app that reports success because the setter did not
throw is lying to its user on every one of those devices. GameCore instead reports
"accepted the request but stayed at 60 Hz" and offers the Shizuku path, which does work
there.
Frame rate gets the same treatment. Reliable per-frame FPS is generally not available through public APIs without root or instrumentation, and what is available varies by OEM and GPU. GameCore detects whether a usable signal exists on the device it is running on, and shows "Not available on this device" when it does not — rather than estimating.
The app is fully usable without it. Shizuku (or a wireless-ADB pairing) raises the app to
ADB-level authority, which is what most devices require for a device-wide refresh-rate
change, for animation scales, for the display's colour keys, for reading and reshaping the
display's own size, for deleting a game's shared-storage cache, for reading and editing a game's
own configuration files, and for a handful of dumpsys reads Android does not expose to ordinary
apps.
When it is absent, capabilities that need it say so and point at the setup screen. Nothing silently degrades into a fake result.
What the app can run with that authority is a closed, enumerated set:
| Program | Used for |
|---|---|
id |
confirming the shell's own uid before trusting it |
cat |
/proc/stat, /proc/meminfo, a battery charge-control node, and a GPU-load /sys node where the device exposes one |
dumpsys |
thermalservice, battery, display, SurfaceFlinger --latency, activity, gfxinfo |
settings get/put |
nineteen specific keys, each with its own value range |
getprop |
three ro.* chipset properties |
pm grant |
three permissions, to this app only |
appops set |
two app-ops, to this app only |
wm size |
reading the display's size, setting a per-game override, and clearing it |
taskset -ap |
reading one game process's CPU affinity, and pinning it to chosen cores |
input tap / input swipe |
tapping, or press-holding, one saved per-game screen point — the volume-button point trigger's one action |
am kill |
closing one named background app, when a profile asks to free memory |
rm -rf |
one game's shared-storage cache directory, on a tap in Game storage |
ls / stat |
listing and sizing files inside one game's own config directory |
cp / mv |
staging an edited config to a temp file and committing it over the original atomically |
rm -f |
removing one staged temp, or one config file you deleted in the editor |
test -w |
probing whether a device's battery charge-control node exists and is writable, before offering the toggle |
sh -c |
writing a single digit (0/1) to that charge-control node — the one shell command in the app, with the value and path passed as positional arguments ($1, $2) rather than spliced into the script |
The one shell command in that list runs a fixed script and receives the value and the path as
positional arguments ($1, $2), never spliced into the script text, so there is no place for a
value or a path to break out of the echo or its redirection. Every other command reaches exec
as an argument vector, with no shell at all. Every
argument that originates outside the app's own code — a package name from a stored profile,
a value from a slider — is validated against a form before a command is built, and a
rejected argument produces no command at all rather than a malformed one. pm grant and
appops set are checked against the compiled application id, so neither can be aimed at
another app.
Every entry below is explained in the app's own Permissions screen too, with a button to the exact settings page that grants it. Nothing is requested until the feature that needs it is switched on.
| Permission | What stops working without it |
|---|---|
SYSTEM_ALERT_WINDOW |
the floating button, pill, crosshair and HUD |
PACKAGE_USAGE_STATS |
automatic game detection and apply-on-launch, and cache sizes |
QUERY_ALL_PACKAGES |
listing installed games so you can make a profile |
KILL_BACKGROUND_PROCESSES |
"free RAM on launch" on a device without Shizuku |
WRITE_SETTINGS |
brightness, screen timeout and orientation lock |
ACCESS_NOTIFICATION_POLICY |
the Do Not Disturb toggle |
MODIFY_AUDIO_SETTINGS |
the volume slider |
WRITE_SECURE_SETTINGS |
colour correction (granted through Shizuku or adb only) |
ACCESS_NETWORK_STATE, INTERNET |
network type, latency and throughput |
POST_NOTIFICATIONS |
any foreground service, since each must post one |
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS |
detection and tracking surviving Doze |
FOREGROUND_SERVICE_* |
the app's foreground services |
Shizuku API_V23 |
the elevated capabilities, if you use Shizuku at all |
Each foreground service declares its real Android 14 foreground-service type rather than one
generic value: dataSync for session tracking, mediaProjection for screen recording with
its own separate consent flow, and specialUse — with a manifest justification for each — for
the overlay, game-detection, performance-monitoring and shake-shortcut services, which is the
closest the platform offers. Nothing runs when it is not needed — each service stops itself.
- Release builds go through R8 with obfuscation and resource shrinking, so a decompiled APK does not hand over the optimization engine's internals or the shell command structure.
- The Room database is encrypted with SQLCipher, and its key is generated in and held by
the Android Keystore — not stored in a file the app could leak. Preferences go through
EncryptedSharedPreferences. android:allowBackup="false". The session database and encrypted preferences never leave the device through a backup transport.- The only intent filter an app can invoke is the launcher activity's. The three others in
the manifest are system-dispatcher actions on services the system binds, never another app: the
notification listener that reads the active media session, the Quick Settings tile, and the
optional accessibility service behind the in-game volume-key trigger. Each is guarded by a
BIND_*permission the system alone holds and no<uses-permission>requests, so being exported makes it reachable bysystem_serverand nothing else. No exported receivers and no deep links, and every activity but the launcher isexported="false". Two content providers exist and neither is a way in: the AndroidXFileProviderthat hands out screenshots, recordings, exports and session cards is not exported and grants a read on one URI at a time, and the Shizuku startup provider must be exported for Shizuku's own server process to bind it, so it is guarded byINTERACT_ACROSS_USERS_FULL— a signature-level permission no ordinary app holds. - The files that leave the device leave by your hand. Screenshots, recordings, session
exports and session cards are written inside the app's own storage and reach another app only
through the share sheet, as a content URI with a one-shot read grant rather than a path, which
is why GameCore asks for no storage permission on any API level. A card's name is a timestamp
and nothing else — a game's own label has no business in a path — at most four are kept before
the older ones are deleted, and the backup rules exclude the whole of
files/from both cloud backup and device transfer. - No cleartext traffic, enforced by a network security config. The only outbound connection the app ever makes is the TCP latency probe, to a host you can change or switch off in Settings.
- All external text is sanitized before storage and again before render. A game's
android:labelis another developer's string and an imported HUD layout is a file from anywhere, so both pass a filter that strips bidi overrides, zero-width characters, control characters and combining-mark stacks, and truncates on a grapheme boundary rather than mid-surrogate. There is a unit test per hostile input, with each invisible character built from its code point so the test file stays reviewable. - No security-relevant behaviour branches on
BuildConfig.DEBUGor on a preference, and no shell output, session data or device identifier is logged in a release build. - Root is detected, not required and not exploited. If the sandbox is not intact, GameCore says so plainly rather than continuing to assume its own guarantees hold.
Kotlin only, Jetpack Compose with Material 3, MVVM, Hilt, Coroutines and StateFlow. No
Java, no XML layouts — the only XML is Android resources and the manifest. 513 source files,
about 132,000 lines.
app/
├── core/
│ ├── common/ Observed<T>, AccessLevel, Formatters, TextSanitizer
│ ├── model/ the domain types — profiles, HUD, colour, sessions, readings
│ ├── permissions/ the catalog and live permission state
│ ├── system/ readers and controllers over the platform APIs
│ ├── overlay/ window geometry, drag/snap arithmetic, the overlay hosts
│ └── shizuku/ the elevated shell, its command set, root detection
├── data/
│ ├── database/ Room + SQLCipher, entities, DAOs, mappers
│ ├── preferences/ EncryptedSharedPreferences, the Keystore-held DB key
│ └── repository/ the only thing the domain layer talks to
├── domain/
│ ├── color/ the colour values, and which sink each one can reach
│ ├── display/ refresh rate and display size, applied and read back
│ ├── gaming/ game detection, profile apply and unwind, session tracking
│ ├── memory/ which apps a reclaim pass may close, and what it measured
│ ├── monitoring/ sampling, aggregation, thermal, battery and latency trends
│ ├── optimization/ OptimizationManager over Standard / Shizuku optimizers
│ ├── overlay/ what is on screen and why
│ └── storage/ per-game cache measurement, and the one delete it offers
├── service/ the four foreground services, screen recording, media listener and quick-trigger services
└── ui/ one package per screen, each a Screen + State + ViewModel
The UI never executes a shell command or touches a system API directly; it reads state and
calls into the domain layer. OptimizationManager chooses between StandardAndroidOptimizer
and ShizukuOptimizer based on what DeviceCapabilityChecker found, and every operation
comes back as one of available, requires Shizuku, requires a permission,
unsupported, applied or failed — there is no boolean that means "probably".
git clone https://github.com/Dreamucxe/GameCore.git
cd GameCore
./gradlew :app:assembleDebug # installable debug APK
./gradlew :app:testDebugUnitTest # JVM unit tests, no device needed
./gradlew :app:assembleRelease # R8 + shrinking; signed if you have a key
./gradlew :app:lintDebug # zero warnings is the standard hereKotlin 2.0.21 · AGP 8.5.2 · Gradle 8.9 · JDK 17 · compileSdk 34 · minSdk 26.
assembleRelease produces a signed APK when a keystore.properties is present at the
project root:
storeFile=release.keystore
storePassword=…
keyAlias=…
keyPassword=…Without it the build still succeeds and emits an unsigned APK, so a contributor with no signing key is not blocked — but an unsigned APK will not install on a device.
The unit tests are deliberately written against the pure, Android-free seams: the geometry, the formatters, the sanitizer, the command builder, the aggregators, the state reducers, the per-session latency fold, and every word the shareable card is allowed to print. 139 suites, 1,819 tests. No mocking framework, no Robolectric, no emulator — the suite runs on any JDK.
There is no backend, no account system, no cloud sync, no analytics, no crash reporting and no advertising SDK. Everything GameCore records — profiles, HUD layouts, sessions, samples — lives in its own encrypted database on the device, and none of it leaves on its own. The session card is the one artefact meant to leave, and it leaves the way a screenshot does: drawn on the device, then handed to whichever app you choose from your own share sheet. GameCore has nowhere of its own to send it.
One thing touches the network, and it is honest about it on the screen it belongs to:
- The latency probe. The single network call GameCore's own code ever makes — a payload-free TCP handshake to a host you choose, timed and closed the instant it connects. It exists because you asked for a ping figure, and it can be switched off. No payload, ever, in either direction. The 3.5 network check reuses this same probe rather than adding one of its own; everything else it reports — the transport, the Wi-Fi band, the link speed — it reads from Android on the device.
MIT — see LICENSE.