Overview
The Bayton Wallpaper App Generator produces a signed Android app that does exactly one thing: set the device wallpaper and keep it set. It is built for dedicated devices, kiosks, signage, and EMM-managed fleets where the wallpaper is part of the deployment rather than a user preference.
The app has a single control - tapping the status line reapplies the wallpaper, re-cropped to the device's current orientation, should an application issue occur. It applies the image at boot, when it is opened, and on a schedule, so a user who changes the wallpaper on the device gets your image back. Its single screen reports what was applied, when, and what went wrong if anything did. Each build produces a unique package name, along with an optional source zip so you can audit or reproduce the build independently.
Android version support
Generated apps target API 37 (Android 17) and require API 28 (Android 9 Pie) or above to install. Devices running Android 8.1 or older are not supported.
Lower API levels are achievable - the minSdk in the generator is a safe baseline that covers every option offered. You can drop the minSdk in app/build.gradle.kts from the downloaded source and rebuild if your fleet needs it.
Test on the API level you actually deploy to. Wallpaper behaviour - particularly whether a device exposes a separate lock-screen wallpaper at all - varies between OEMs and Android releases more than most platform surfaces do.
Signing
- Debug
- Used for local testing purposes only, should not be used in production and cannot be deployed through Google Play.
- Release signed
- Release-signed using Bayton's certificate. No setup required - build and deploy immediately. Each build is assigned a unique package name. Use an update code to reuse the same package name for updates. AAB uploads to Managed Google Play are unaffected; Play App Signing takes over after first upload. Not intended for public Google Play Store distribution: under Google's developer verification, the publishing account must own both the signing key and the package name, so Release-signed builds are suitable for Managed Google Play, direct EMM push, and sideload via ADB - see developer verification for what's required to install on non-managed consumer devices. For public Play distribution, use Self signed with a keystore registered to your own verified developer account.
- Self signed
- Sign with your own certificate for full ownership. Upload a JKS or PKCS12 keystore with the key alias and passwords. Always use a unique keystore for builds produced by this generator - never share it with another production app, and never reuse a keystore that already signs anything you can't afford to compromise. If the signing key is ever compromised or lost, you only lose this app; a shared key would cascade the impact across every other app it signs. Credentials are never stored and are purged immediately after the build completes.
Developer verification (Google)
From September 2026, Android enforces a developer verification requirement on certified Android 7+ devices: apps must be registered against a verified Google developer account (Play Console or Android Developer Console) to install on consumer devices. Enforcement begins in Brazil, Indonesia, Singapore, and Thailand, with global rollout from 2027. Managed-device deployments are exempt.
- Release-signed builds (Bayton's key)
- Bayton owns the signing key and the generated package namespace under its own verified account. Works: Managed Google Play (private apps via the EMM iframe) and direct EMM push to managed devices - both exempt as managed-device deployments. Doesn't work: public Google Play Store (a different developer account cannot upload an APK signed under Bayton's certificate) and direct consumer sideload on non-managed certified devices in regulated regions (the end user would need to opt into Android's advanced flow - a one-time setup with a 24-hour wait and biometric confirmation - or install via ADB).
- Self-signed builds (your key)
- You own the key and the package name, so you own the verification path. For public Play Store distribution, the keystore must be registered to a verified Play Console developer account that owns the package name. For consumer sideload on certified devices in regulated regions, the package must be registered against an Android Developer Console account (full distribution, or limited distribution for fleets of up to 20 devices). For managed-device EMM deployment, no verification is required.
- ADB / development install
- ADB installs are not subject to verification and continue to work as-is for any signing mode. This is the path for local testing and engineering builds against attached hardware.
The wallpaper image
- Wallpaper URL
- Fetched at runtime, and the flexible option: the URL is exposed as a managed-configuration key, so your EMM can point the whole fleet at a different image without a rebuild or a reinstall. Must be https unless you tick Allow plaintext HTTP.
- Bundled image
- Uploaded at build time and baked into the APK, up to 20 MB. Always works, including on a device with no network at all, but changing it means building again. Not stored server-side - re-attach it on each rebuild.
- Both together
- The app fetches the URL and falls back to the bundled image when it cannot be reached. This is the most robust combination for a fleet: remotely changeable, but never blank.
- Allow plaintext HTTP
- Off by default, and only relevant for an
http:// address. A wallpaper fetched over plaintext can be swapped in transit, and whatever arrives is what everyone sees on every unlock. Intended for operators on an internal network without TLS.
- Trust user-installed CAs
- On by default. Adds the device's user CA store to the app's trust anchors alongside the system store, which is what makes an image served from behind a corporate SSL-inspection proxy (Zscaler, Netskope, Forcepoint) or an MDM-distributed self-signed certificate work. Turn off to restrict trust to the Android system CA store only.
Where & when it applies
- Apply to
- Android has two independently settable wallpapers - home and lock. Choose either, or set both to the same image. Some devices and launchers do not expose a separate lock-screen wallpaper; where that is the case the app reports it on its status screen rather than failing quietly.
- The four triggers
- The wallpaper is applied at device boot (a forced re-apply, since a user may have changed it), when the app is opened, on the periodic check, and by a tap on the status line - the one manual control, a forced recovery reapply re-cropped to the device's current orientation. Between them, a device converges back on the operator's image without anyone touching it.
- Check for changes
- Sets that period. The service accepts 15 to 1440 minutes and rejects anything outside that with a 400 - it does not silently coerce. The floor is WorkManager's: it will not run periodic work more often than every 15 minutes, so a smaller value would be a promise the platform cannot keep. The ceiling is a day, beyond which a managed-configuration push would take too long to land to be useful. This is the ceiling on how long a fleet-wide change takes to reach an idle device, not the only chance it gets.
- Why a poll at all
- Android delivers
ACTION_APPLICATION_RESTRICTIONS_CHANGED only to dynamically registered receivers, never to manifest-declared ones - and an app with no long-lived component has nowhere to register one. The periodic check is what makes a managed-configuration push land on a device sitting idle in a drawer. It is a deliberate design decision, not a workaround.
Deploying to devices
- It must be started once before it runs unattended
- Until it is first launched the app sits in Android’s stopped state, and the platform delivers no broadcasts to a stopped app. A device where it has never been opened therefore never receives
BOOT_COMPLETED, never schedules its periodic check, and never sets the wallpaper - it is installed and inert, which looks exactly like a broken build. Android’s own wording: “Applications are in a stopped state when they are first installed but are not yet launched” (Android 3.1 platform notes).
- Three ways to handle that
- Custom DPC - auto-start the app after install, and everything runs unattended from then on. AMAPI - define an app role permitting the app to start in the background, for the same result without a custom DPC. Neither available - the app has to be opened manually once per device; after that first launch, boot, app open and the periodic check all work normally. This is not specific to this app: it applies to anything relying on
BOOT_COMPLETED or scheduled work without a launcher interaction.
- A wallpaper policy blocks it entirely
- If your EMM sets the
DISALLOW_SET_WALLPAPER user restriction, the apply is refused however the app is started. SET_WALLPAPER does not override it - only a device owner or profile owner may set the wallpaper regardless of that restriction, and a generated app is neither. The app reports the refusal on its status screen rather than failing silently, but the wallpaper will not change. Clear the restriction on any fleet where this app is expected to work.
- Kiosk and single-app
- Past that first launch the app needs no foreground time, so it sits alongside a lock-task deployment. It does not need to be the home app, and does not need to be launchable by the user.
Managed configuration
The generated app publishes four managed-configuration keys, so your EMM can change an installed fleet without a rebuild. Every one of them defaults to the choice you make on this form, so a device that has never received a push behaves exactly as you built it.
wallpaper_url
- The image to fetch. Push a new one and every device converges at its next check, boot, or launch. Pushing a blank value falls back to the bundled image.
apply_to
home, lock or both. Moves the image between the two wallpaper slots without a rebuild.
check_interval_minutes
- How often the app re-reads this configuration. Accepts 15 to 1440. A value outside that range is ignored and the build-time setting stands - it does not discard the rest of the push along with it.
allow_http
- Can only tighten. A build that permits plaintext exposes this key already switched on, so you can revoke plaintext across a fleet without a rebuild. You cannot grant it: a build made HTTPS-only blocks cleartext in the platform itself, so switching this on achieves nothing. Revoking it while an
http:// URL is still configured makes the app refuse to fetch and report why, rather than quietly continuing over plaintext - change the URL to HTTPS in the same push.
Capabilities and limitations
- It cannot clear the wallpaper. Pushing a blank URL through managed configuration falls back to the bundled image. This app exists to set a wallpaper, so "set nothing" is not one of its states.
- Images are centre-cropped to the device screen, with a pixel ceiling applied so a very large image is downsampled rather than being allowed to exhaust memory. EXIF orientation is honoured.
SET_WALLPAPER does not override DISALLOW_SET_WALLPAPER. Only a device owner or profile owner may set the wallpaper regardless of that restriction, and a generated app is neither - so if your EMM sets it, the apply is refused and the app says so on its status screen.
- Redirects cannot downgrade the transport. The scheme is re-checked after every redirect, so an https URL cannot be quietly walked to http mid-fetch.
- It reports back through keyed app states -
configuration_status, wallpaper_status (alert on this one: it carries a policy block or a failed fetch), wallpaper_source (url or bundled, so a device silently on the fallback is visible), last_applied and apply_to. A device still in Android’s stopped state reports nothing at all, because the app has never run - silence is not success.
- The app needs no foreground time. It never has to be opened, and does not need to be launchable by the user, so it sits happily alongside a lock-task or kiosk deployment.
Output format
- APK
- A ready-to-install package. Suitable for direct sideloading or silent push via an EMM using managed app distribution.
- AAB
- Android App Bundle, the preferred format for Managed Google Play private app distribution. Google Play repackages and signs it through Play App Signing after upload. AAB cannot be sideloaded directly.
Theme colour
Sets the colour used for the app's single status screen and its system bars. It does not affect the wallpaper - the image is applied exactly as fetched. Accepts any CSS hex colour (#rrggbb or #rgb), and defaults to white. Text and icon contrast is computed automatically using WCAG relative luminance, so dark colours receive white foregrounds and light colours receive dark ones.
Update codes
Every new build generates a unique update code returned once in the submission response. Store it securely - it is never shown again. Only a SHA-256 hash is kept server-side, so the raw code cannot be recovered or regenerated; this is the security boundary that lets it work as a one-time recovery secret.
When you need to update an existing app and must keep the same Android package name (required for EMM silent-update flows and Managed Google Play uploads beyond the first), enter the original update code in the Update code field before submitting. The service will reuse the registered package name for the new build.
If you do not have an update code, a new unique package name is assigned. You can replace the app on managed devices by uninstalling the old version first or by pushing the new package as a separate app.
Config export / import
Every successful build offers a Download config button next to Download source - a portable JSON file (wallpaper_config.json) capturing every form field plus the build's update_code. Use it to skip refilling the form on later rebuilds.
To re-import, click Import config at the top of the form and pick a previously-downloaded JSON. Every field populates, including radio selections and Options toggles. Icon and keystore are not in the JSON (binary / sensitive). The icon is also not stored server-side, so it will need to be provided again on subsequent builds; submit without one and the default is used.
The exported file contains your update code in plaintext, so treat it like the code itself - it's effectively a per-app one-time recovery secret. The file expires alongside the artefact (5 min after first download) so the raw code doesn't linger server-side.
REST equivalent: POST the JSON as the config_json form field on /api/build. Other form fields submitted in the same request override the imported values, so a one-off colour change or output-format swap is one extra field, not a JSON re-edit.
App icon
Upload a 512 × 512 px PNG for best results. The generator produces all required mipmap densities automatically, including an adaptive icon for Android 8.0 and above. Optionally maskable icons with safe-zone padding produce better results in launchers that apply custom shapes. A default Bayton icon is used when no icon is provided.
Neither the icon nor the bundled wallpaper is stored, and both will need to be provided on subsequent builds. The config export / import flow round-trips every form field but those two - it records that an image was bundled, not the image itself - so re-attach them manually when rebuilding from a previously exported wallpaper_config.json or against a typed update_code. Submit without an icon and the default is used.
Downloads and expiry
Download links expire 5 minutes after first click and build files are purged at that point. All builds are purged after 24 hours regardless of download activity. The source download includes post-substitution Kotlin, XML, and Gradle files so you can audit exactly what was compiled or reproduce the build with your own tooling. A source rebuild matches ours byte-for-byte except the versionCode, which is intentionally derived from build time so every build outranks the last for Play and EMM update ordering - for that reason, don't push a self-built APK to the same distribution channel as ours.
Versioning & provenance
The PAG server itself is versioned (semver). Every APK is stamped with the server version that produced it, plus a build timestamp. Five ways to read the stamp, ranked by ease:
- The download filename - the APK comes back as
<package>_<job-id>_genver-<version>.apk (e.g. org.bayton.gen.wallpaper.pa1b2c3_58036933df5a_genver-0.1.0.apk). At-a-glance lookup; lost if the file is later renamed.
- Embedded text file -
unzip -p <file.apk> assets/pag-build.txt returns three lines: builder: pag, version: ..., build_timestamp: .... Universal unzip tool; works after rename.
- Greppable -
strings <file.apk> | grep '^builder:' surfaces the same three lines without any extraction.
- On-device:
adb shell dumpsys package <pkg> | grep org.bayton.pag reads the manifest meta-data tags.
- From the build status / live server: the
api/status/<id> response includes builder_version; GET api/version returns the current PAG release.
Quote the version in any support ticket so the issue can be tied to the exact build of PAG that produced your artefact.
Best practices
- For Managed Google Play distribution, use Release signed or Self signed with AAB output.
- For direct EMM app push, use Release signed or Self signed with APK output.
- Set a wallpaper URL and upload a bundled image. You then get remote changes through managed configuration, and a device that cannot reach the URL still has something to apply.
- Size the image for the largest screen in your fleet. It is centre-cropped to each device, so anything important belongs near the middle.
- Leave the check interval on hourly unless you have a reason. It is the ceiling on how long a fleet-wide change takes to land, and the app also re-checks at every boot and launch.
- Keep the URL on https. Plaintext is an explicit opt-in because whatever arrives over it is displayed on every unlock.
- Package names are unique per build and permanently logged. Use an update code to rebuild with the same package name. Once uploaded to Google Play a package name cannot be changed.
- Download the source alongside your build to retain an auditable record of what was compiled.