# BetterKnownInstalled: rewriting install metadata so DroidGuard stops guessing

> BetterKnownInstalled is a Magisk, KernelSU and APatch module that edits the packages.xml of a rooted Android device so that user installed apps carry Google Play Store attribution, which is what DroidGuard reports as UNKNOWN_INSTALLED. It patches attributes in a single streaming pass and keeps the original values in a metadata.db so an uninstall can restore them exactly.

**Pixel-Props/BetterKnownInstalled** — Patches packages to fix DroidGuard UNKNOWN_INSTALLED issues. (Magisk/KernelSU/APatch)

- Repository: https://github.com/Pixel-Props/BetterKnownInstalled
- Stars: 576 · Forks: 20
- Language: Shell
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/pixel-props-betterknowninstalled

## What DroidGuard reads and what the module changes

Android's package manager keeps a file called packages.xml that records how every installed package got there. DroidGuard, the Google Play Services component that verifies app installations, reads several attributes in that file to decide whether an app was installed normally. When it cannot tell, the app is reported with an UNKNOWN_INSTALLED status, which surfaces as installation and verification problems on devices that have Play Services.

BetterKnownInstalled is a Magisk, KernelSU and APatch module that patches those attributes so DroidGuard sees a legitimate store installation. It targets user installed apps only, defined by a codePath under /data/app/, so system, vendor and APEX packages are skipped. It also treats system="true" and system="1" as a safety guard and skips those packages even if they somehow match the path filter.

For each targeted package element, three attributes are set to the Play Store's identity: installer, installInitiator and installerUid become com.android.vending and the corresponding user ID. The installOriginator attribute is removed, because it can cause conflicts. The isOrphaned and installInitiatorUninstalled attributes are removed when their value is true or 1, since those values flag an app as incorrectly installed. Finally, packageSource is set to the constant 2, which represents PACKAGE_SOURCE_STORE, and it is added when it is missing rather than only replaced when present.

## Binary XML and the converter binaries shipped with the module

The file being patched is not a text file. packages.xml is typically stored in Android's binary XML format, so a tool that edits attributes has to convert between the two representations. The module uses the abx2xml and xml2abx binaries for that, and bundles architecture specific copies for aarch64, armv7aeabi, i686, x86_64 and riscv64, so the module installs the converter that matches the device it is running on.

The credit for those binaries goes to rhythmcache, also known as winter, through the android-xml-converter project, and the module uses them only if the system does not already provide them. The aarch64 and armv7aeabi builds cover the phones most people own, while i686, x86_64 and riscv64 cover emulators and the less common device families.

That list is a practical detail for anyone testing the module on a non standard device, since a converter built for the wrong architecture will fail at the first conversion step rather than at the first patch. The conversion is also why the module needs to read the whole file into a workable form before it can make a change, which is the design constraint the current version works around.

## Version 1.6.1 replaces blind patching with a metadata database

Earlier behaviour of the module was to patch every app on every boot. The current design, described in the project documentation under how it works for v1.6.1, keeps a lightweight metadata.db in the module directory and patches packages.xml with a single streaming pass that edits one package element at a time.

The database stores the original values of the attributes the module touches for every user app, namely installer, installerUid, installInitiator, packageSource, installOriginator, isOrphaned and installInitiatorUninstalled. The three boot paths are then simple. On the first boot the module scans packages.xml, builds the database with those original values, then applies the patches. On later boots it prunes removed apps from the database, detects newly installed apps, appends their original values, and re-applies the patches. The pre-split and the patch are idempotent, so repeated boots do not modify or grow the file further. On uninstall the module reads the database, restores the exact original values per app, verifies the result, and then deletes the database.

The restore path handles both text and binary XML and the typed -int and -bool attribute variants, which is where a naive implementation tends to produce a file the package manager will not accept. The patch engine also preserves the original file formatting byte for byte outside the packages it touches, handles packages.xml whether its attributes sit on one line or are wrapped across lines, and produces byte identical output on repeated runs. A single packages.xml.safety copy is kept in the module directory for crash recovery.

## Installing it, and the two ways uninstall can go wrong

Installation happens through the module manager of the root solution in use, Magisk Manager, KernelSU or APatch, followed by a reboot. The documentation is emphatic about one point: installation from recovery is not supported, and the module must be installed from within a running Android environment. After the first reboot the module does its work during the boot process, and no further interaction is required, so there is no app to open and no toggle to flip.

Uninstall is where the design pays off, but it has two failure modes. When you remove the module and reboot, uninstall.sh restores each app's original installation metadata from metadata.db before the module is cleaned up. If that database is missing, for example because it was deleted by hand, restoration cannot happen and the patched values stay in packages.xml. A safety copy of the pre-restore file is also kept under /data/local/tmp/BKI_restore_backups/, which is a separate copy from the in-module packages.xml.safety file used for crash recovery during normal operation.

Activity is logged to /data/adb/modules/BetterKnownInstalled/BetterKnownInstalled.log, which is the first place to look when an app is still being reported as incorrectly installed after a reboot.

## The debug harness lets you rehearse the patch on a dump

The repository ships a regression harness that runs the real module functions against a packages.xml dump instead of a live device. The tool is tools/bki_debug.sh, and it takes the path to the two shell scripts the module runs at boot and at uninstall, followed by the XML file to work on:

```sh
BKI_PFD=/path/to/module/post-fs-data.sh \
BKI_PFD_UN=/path/to/module/uninstall.sh \
sh tools/bki_debug.sh packages.xml
```

The dump can be produced with abx2xml, which is the same converter the module relies on, so the test input matches the format the module actually meets. The script checks uid resolution, database build, prune and update, patch semantics including the absence of duplicate attributes and well formed output, the end to end process_xml step, byte identical idempotency, and the uninstall restore round trip. The expected result is a line reporting a number of passed checks and zero failures.

This is the part of the project that makes the rest reviewable. Everything the module does to a device is a shell script, and the harness exercises those same scripts against a file, so the behaviour can be checked before it touches a real installation.

## The trade-off is that you are editing a trust signal

The module's own documentation carries a warning that is worth reading in full before anyone installs it. The environment check verdict modification implemented by the module is described as a relatively new technique, and Google's implementation of that check cannot yet guarantee that all packages will be correctly identified. The documentation says plainly that some apps might still trigger Play Protect warnings or behave unexpectedly even after the module is used, because of limitations and inconsistencies in how Google verifies installations.

The honest way to frame this is that the module does not remove a warning, it rewrites the metadata that the check reads. It says an app came from the Play Store, and it says that whether or not it did. That is a legitimate trade for a developer sideloading their own build of an app, and it is a poor trade for someone who relies on that check to tell them that an APK from an unknown source is not trustworthy. The patched values are also invisible to the person looking at the device, which means the change in behaviour is silent.

A second practical risk is that the module touches a file the package manager depends on. The safety copy, the idempotency guarantees and the restore path all exist to limit the damage, and the debug harness gives you a way to check the logic, but a failure during boot is still a failure on a device that might be your only phone. The documentation does not describe a repair procedure beyond the safety copy, and it does not claim that a partially patched file will be detected for you.

## Release rhythm, repository layout and the GPL-3.0 terms

The project is not archived, and the last push was on 2026-09-11. Releases are frequent for a module of this kind: version 1.6.1, tagged 1610, was published on 2026-09-11, version 1.6.0, tagged 1600, on 2026-09-07, and version 1.5.2, tagged 1520, on 2026-09-03. The numbering scheme pairs a release version with a build number, which fits a module distributed through update.json, the update channel file at the top level of the repository.

The layout follows the conventions of a Magisk style module. META-INF holds the install metadata read by the manager, customize.sh is the installation script, post-fs-data.sh is the boot-time patch script, uninstall.sh the restore script, and util_functions.sh and the common directory hold shared shell helpers. module.prop carries the module name and version, CHANGELOG.md the release history, and a tests directory the test assets.

The licence is the GNU General Public License version 3.0, with the terms in the LICENSE file. For a shell module that is mostly a set of scripts, the copyleft obligations attach to derived versions of those scripts, and the binary converters are credited to a separate project with its own terms, so anyone redistributing a modified build should check both.

## Conclusion

BetterKnownInstalled makes sense for a developer or power user who sideloads their own builds onto a rooted device and needs the install attribution to look like a store install, and who is comfortable with a module that rewrites a system file at every boot. It is the wrong tool for an unrooted device, for anyone who wants Play Protect to keep warning them about sideloaded apps, and for a device where the original packages.xml backup is not kept, because uninstall restores only from metadata.db and a missing database leaves the patched values in place. Before installing, back up /data/app state, confirm your recovery environment cannot install the module since installation from recovery is unsupported, and read the project warning that Google's check cannot guarantee every package is identified correctly.

## FAQ

### What does the BetterKnownInstalled module do on Android?

It patches packages.xml during boot so that user installed apps carry Google Play Store attribution, which addresses the UNKNOWN_INSTALLED status reported by DroidGuard. It only touches packages whose codePath is under /data/app/ and skips anything marked system="true" or system="1".

### How do I install and remove the BetterKnownInstalled module?

Install it through Magisk Manager, KernelSU or APatch and reboot, since installation from recovery is not supported. Removing the module and rebooting triggers uninstall.sh, which restores the original attribute values from metadata.db before deleting it, and if that database is missing the patched values remain in packages.xml.

### Can I test BetterKnownInstalled without installing it on a phone?

Yes. The repository ships tools/bki_debug.sh, which runs the real post-fs-data.sh and uninstall.sh functions against a packages.xml dump, checking uid resolution, database handling, patch semantics, idempotency and the uninstall restore round trip. A dump can be produced with the abx2xml converter the module also uses.

### Does BetterKnownInstalled guarantee that Play Protect stops warning?

No. The project documentation warns that Google's implementation of the environment check cannot yet correctly identify all packages, so some apps may still trigger Play Protect warnings or behave unexpectedly. The module rewrites install metadata rather than removing the check.

## Sources

- [Issues](https://github.com/Pixel-Props/BetterKnownInstalled/issues)
- [License: GPL-3.0](https://github.com/Pixel-Props/BetterKnownInstalled/blob/main/LICENSE)
- [Pixel-Props/BetterKnownInstalled on GitHub](https://github.com/Pixel-Props/BetterKnownInstalled)
- [README](https://github.com/Pixel-Props/BetterKnownInstalled/blob/main/README.md)
- [Releases](https://github.com/Pixel-Props/BetterKnownInstalled/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pixel-props-betterknowninstalled
