This document captures how the app is structured today so you can change it confidently. Keep it close when you explore the code base.
- ShaderEditor is an Android GLSL playground/live wallpaper. Users edit fragment shaders, preview them live, import textures/uniforms, and set any shader as a wallpaper (
README.md). - The whole product lives in a single Android application module under
app/. Source is Java (language level 17) with Gradle Kotlin build scripts. - The UI is a single-activity, multi-fragment setup with custom widgets for the editor, shader preview, uniform pickers, and texture tooling.
- Sensors, camera, microphone, battery status, wallpaper offsets, notification counts, etc., are exposed to shaders as uniforms via
ShaderRenderer.
- Requirements: Android Studio Ladybug+, Android SDK 21–36, Java 17, and the Android NDK is not required. Gradle wrapper + Kotlin DSL (
build.gradle.kts,app/build.gradle.kts:1). - Build variants:
debugandrelease; release is minified/resource-shrunk and expects signing env vars (ANDROID_KEY_*). - View binding +
BuildConfiggeneration are enabled. Compose is not used. - Helpful commands:
./gradlew assembleDebug(ormake debug) – build APK../gradlew assembleReleaseand./gradlew bundleRelease– ship builds../gradlew lintDebug,make lint,make infer,make avocado– lint/static analysis/drawable checks.make install,make start,make uninstall– adb helpers.
- Version catalog (
gradle/libs.versions.toml:1) centralizes dependency versions (AppCompat 1.7, Material 1.12, Preference 1.2, CameraX 1.4, AGP 8.12). - Fastlane metadata lives in
fastlane/for Play/F-Droid; SVG assets are undersvg/.
| Path | Notes |
|---|---|
app/src/main/AndroidManifest.xml:1 |
Permissions (camera, mic, legacy storage), wallpaper + notification services, exported activities. |
app/src/main/java/de/markusfisch/android/shadereditor/ |
All runtime code. Key packages: activity, fragment, widget, opengl, database, hardware, io, view, adapter. |
app/src/main/res/ |
Layouts, drawables, fonts, res/raw/*.glsl sample shaders, res/xml/preferences.xml. |
res/xml/backup_rules.xml, res/xml/extraction_rules.xml |
Backup configuration referenced by the manifest. |
fastlane/metadata/... |
Store descriptions/screenshots. Keep in sync for releases. |
CONTRIBUTING.md, FAQ.md, CHANGELOG.md |
Ground rules, docs referenced in-app. |
ShaderEditorApp(app/src/main/java/de/markusfisch/android/shadereditor/app/ShaderEditorApp.java:1) is theApplication. It initializes the singletonPreferences, prewarmsDatabase, keeps a process-wideUndoRedo.EditHistory, turns on StrictMode in debug builds, and registersBatteryLevelReceiver(API ≥ 24) to drive low-power behavior.- Splash flow:
SplashActivityimmediately launchesMainActivityso the app initializes before the UI is drawn (activity/SplashActivity.java:1).
MainActivity(activity/MainActivity.java:1) hosts everything. It instantiates:EditorFragment(code editing).ShaderViewManager(GLSurfaceView preview + quality spinner).ShaderManager(load/save shaders, handleACTION_SEND/ACTION_VIEWintents, duplicate/delete, persist thumbnails).ShaderListManager(ListView + background loader for saved shaders).UIManager(toolbar buttons, drawer, toggling editor visibility, extra keys).MainMenuManager(popup menu with editor actions, navigation to Add Uniform, samples, settings).NavigationManager(previews, FAQ link, share intent).ExtraKeysManager(soft keyboard helpers + completions strip).
- State flow: the editor notifies
ShaderManagerwhen text changes;ShaderManagerupdatesShaderViewManagerandShaderListManager; UI state (title/subtitle, show-errors pill) is adjusted viaUIManager.
EditorFragment(fragment/EditorFragment.java:1) wires the customShaderEditorwidget to undo/redo, completion listeners, and preference-driven appearance (font, ligatures, line numbers, update delay).ShaderEditor(widget/ShaderEditor.java:1) extendsLineNumberEditText. Responsibilities: syntax highlighting (highlighter/*), debounce compile requests, auto-insert braces, convert ShaderToy snippets, completion entry, lint error highlighting. Highlights/errors are recalculated on a worker thread (TokenListUpdater).UndoRedo(view/UndoRedo.java:1) tracks a shared, process-wide edit history so the editor can preserve undo stacks between fragment recreations (history object is kept inShaderEditorApp.editHistory).ExtraKeysManager(activity/managers/ExtraKeysManager.java:1) shows tab/brace buttons and completion chips, auto-hiding when the IME is closed if preferences demand.- Compile errors from the GL renderer are visualized by
ErrorListModalandErrorAdapter(widget/ErrorListModal.java:1,adapter/ErrorAdapter.java:1).
ShaderView(widget/ShaderView.java:1) wraps aGLSurfaceView, sets the GLES client version, installs a custom EGL context factory (prefers GLES3 but falls back to GLES2), forwards touch events to the renderer, and scrubs non-ASCII characters before sending shader source to OpenGL.ShaderRenderer(opengl/ShaderRenderer.java:1) is the core engine:- Parses shader code for
sampler2D,samplerCube, andsamplerExternalOESuniforms, manages backbuffer FBOs, thumbnails, and resolution scaling via the "quality" multiplier. - Exposes dozens of uniforms (time, resolution, multi-touch, sensors, battery, day/night, notification counts/time, camera frames, mic amplitude, wallpaper offsets, etc.). Sensor listeners live in
hardware/*(e.g.,AccelerometerListener,CameraListener,MicInputListener, etc.) and are registered only when the shader actually references the relevant uniform. - Taps Android services:
NotificationServicefor notification uniforms,BatteryLevelReceiverfor power state, media volume viaAudioManager, wallpaper offsets, etc. - Uses
TextureBinder,TextureParameters, andBackBufferParametersto manage imported textures/backbuffer configuration. - Publishes FPS + compile info logs back to UI threads via
ShaderRenderer.OnRendererListener.
- Parses shader code for
ShaderViewManager(activity/managers/ShaderViewManager.java:1) coordinatesShaderViewlifecycle with the activity, exposes the quality spinner, and pushes renderer events toMainActivity.PreviewActivity(activity/PreviewActivity.java:1) runs shaders fullscreen when the user prefers manual runs. Its staticRenderStatusobject transports FPS/errors/thumbnail data back toMainActivity.
ShaderWallpaperService(service/ShaderWallpaperService.java:1) hosts aGLSurfaceViewinside a live wallpaper engine. It listens for preference changes (Preferences.WALLPAPER_SHADER,SAVE_BATTERY), enforces low-power render modes when the receiver marks the battery low, and reusesShaderView/ShaderRenderer.NotificationService(service/NotificationService.java:1) is a notification listener. Its static getters feedShaderRendereruniformsnotificationCountandlastNotificationTime. It prompts the user to enable the listener when needed.BatteryLevelReceiver(receiver/BatteryLevelReceiver.java:1) toggles low-power mode and power-connected flags and updates wallpaper render modes.
Database(database/Database.java:1) is a singleton façade aroundSQLiteOpenHelperwith schema version 5. It builds tables viaDataSource.buildSchema()and exposes DAOs.DataSource(database/DataSource.java:1) bundles the DAOs and is retrieved everywhere viaDatabase.getInstance(context).getDataSource(). Never cache rawSQLiteDatabaseobjects outside DAO scope.ShaderDao(database/dao/ShaderDao.java:1):- CRUD for shaders, thumbnails (PNG blobs), and quality multiplier.
insertShaderFromResourceseeds new shaders fromres/raw/*.glsl.- Column definitions live in
DatabaseContract. - Names are optional; UI falls back to the last-modified timestamp (
DataRecords.ShaderInfo.getTitle()).
TextureDao(database/dao/TextureDao.java:1):- Stores user-imported 2D textures and cube maps (ratio differentiates them).
- Saves both thumbnails (PNG) and full-resolution PNG matrices. Dimensions feed sampler defaults.
DataRecords(database/DataRecords.java:1) holds immutable records used throughout UI/DAOs.- Import/export:
ImportExportAsFiles(io/ImportExportAsFiles.java:1) reads/writes.glslfiles underDownloads/ShaderEditor. Uses legacy storage APIs on Android < Q; guarded by runtime permissions fromPreferencesFragment.DatabaseImporter/DatabaseExportersupport SAF-based SQLite exports/imports.- Texture import flows go through
BitmapEditorandTextureDao.
- Adding uniforms:
AddUniformActivity(activity/AddUniformActivity.java:1) hostsUniformPagesFragment(ViewPager with presets, 2D textures, cube maps). The activity registers ActivityResult launchers for picking/cropping images and returns the selected GLSL statement in the result bundle.UniformPresetPageFragment(fragment/UniformPresetPageFragment.java:1) shows the built-in uniform list fromPresetUniformAdapter. Sampler presets launchTextureParametersFragment.UniformSampler2dPageFragmentandUniformSamplerCubePageFragment(fragment/UniformSampler2dPageFragment.java:1,fragment/UniformSamplerCubePageFragment.java:1) list user textures viaTextureAdapter, async-loaded throughTextureDao.TextureViewFragment/TextureViewActivitylet users preview a texture, delete it, or insert the uniform statement for it.TextureParametersFragment(fragment/TextureParametersFragment.java:1) formats sampler parameters (wrap, filter, backbuffer options) before returning the statement toAddUniformActivity.
- Texture creation:
CropImageActivity+CropImageFragment(activity/CropImageActivity.java:1,fragment/CropImageFragment.java:1) handle picking and cropping an image prior to import.Sampler2dPropertiesFragment,CubeMapActivity,CubeMapFragment, andTextureParametersView/BackBufferParametersView(underwidget/) finalize sampler metadata.
- Samples:
LoadSampleActivitywithLoadSampleFragment(activity/LoadSampleActivity.java:1,fragment/LoadSampleFragment.java:1) surfaces sample shaders fromres/raw/sample_*.glslviaSamplesAdapter(adapter/SamplesAdapter.java:1).ShaderManager.loadSampleLauncherinserts the sample into the DB and selects it.
- Preference keys/constants live in
Preferences(preference/Preferences.java:1). Important toggles:- Run mode (auto vs manual vs manual-with-preview activity).
- Update delay/debounce for shader recompiles.
- Editor styling (font, tab width, ligatures, extra keys, line numbers).
- Auto-save, save-on-run, default shader template ID, default wallpaper shader, "show extra keys".
- Sensor delay, hide native IME suggestions.
- Import/export directory preferences (only used for labeling).
PreferencesFragment(fragment/PreferencesFragment.java:1) rendersres/xml/preferences.xml, requests runtime permissions for file I/O, and wires preference click listeners for import/export, database backup, etc. It also updates preference summaries (shader pickers viaShaderListPreference/ShaderListPreferenceDialogFragment) and triggersBatteryLevelReceiverwhen save-battery changes.- Many runtime checks reference
ShaderEditorApp.preferences. Always read/update through this singleton to keep UI/resume logic consistent.
- Sensors (accelerometer, gyroscope, gravity, rotation vector, magnetic field, light, linear acceleration, pressure, proximity) share a base
AbstractListener(hardware/AbstractListener.java:1).ShaderRendererregisters/unregisters them based on uniforms in the shader source. Sensor delays honor the preference set in settings. - Camera textures use
CameraX(hardware/CameraListener.java:1) withProcessCameraProviderandSurfaceTexture. Both front/back cameras are exposed, along with orientation matrices and addent vectors (UNIFORM_CAMERA_ADDENT,UNIFORM_CAMERA_ORIENTATION). The wallpaper engine acts as theLifecycleOwnerwhen rendering live wallpapers. - Microphone input (
hardware/MicInputListener.java:1) samples amplitudes viaAudioRecord. RECORD_AUDIO permission must already be granted (no custom request flow here). - Notifications require the Notification Listener permission;
NotificationService.requirePermissions()prompts with a dialog. - Media volume uniforms query
AudioManagereach frame. - Battery events rely on
BatteryLevelReceiver, coupled with thesave_batterypreference to switchGLSurfaceViewrender modes.
ShaderWallpaperService– see §4.5.NotificationService– see §4.5.BatteryLevelReceiver– see §4.5.ShaderWallpaperServicetoggles the manifest-enabled state ofBatteryLevelReceiverbased on lifecycle to avoid wakeups when not running.ShaderWallpaperService.ShaderWallpaperViewextendsShaderViewbut overridesgetHolder()to render into the wallpaper surface.
- File import/export only works on API < 29 because scoped storage removed direct SD access; for newer versions the preference entries are hidden (
PreferencesFragment.wireImportExport()). - Database import uses the Storage Access Framework with MIME types
application/x-sqlite3,application/vnd.sqlite3, andapplication/octet-stream. - Texture imports rely on SAF
ACTION_GET_CONTENTflows and optionally cropping/resampling to avoid OOM (seeBitmapEditor.getBitmapFromUri()). - When exporting shaders, tabs can be converted to spaces based on
export_tabspreference before sharing (handled insideNavigationManager.shareShader()).
- Follow
CONTRIBUTING.md: tabs for indentation, EditorConfig is authoritative, align with Android Studio defaults otherwise. Keep commits focused (one feature per commit) and prefer squash merges. - Most helpers live under
activity/managers/; resist the urge to introduce new global singletons—prefer scoped manager classes or fragments. - Shared state flows through the
ShaderManager/ShaderListManager/ShaderViewManagertriad. If you add new shader metadata, update all of them plus the DAO and UI surfaces. - Rendering code runs on GL threads; do not touch UI widgets from inside
ShaderRenderer. UsequeueEvent()or the provided listener callbacks to hand results back to the main thread. - Keep database interactions inside DAOs; they already open/close databases in
try-with-resources. Long-running reads are dispatched to single-thread executors + main-thread handlers (follow the patterns inShaderListManagerandUniformSampler2dPageFragment). - When adding uniforms or sensors, define new constants in
ShaderRenderer, expose them inPresetUniformAdapter, and wire end-to-end (renderer uniform location, value assignment, UI copy). - Build/res dependencies:
- Material components for dialogs, tabs, snackbars.
androidx.preferencefor settings.CameraXfor live camera textures.- No Jetpack Compose/Navigation; fragments are added manually via helper methods in
AbstractSubsequentActivity.
- The project doesn’t ship automated unit/instrumentation tests. Rely on manual testing, lint (
./gradlew lintDebug), and static analyses (make infer). - Debug builds enable StrictMode (thread + VM violations), so fix any disk/network access on the main thread before shipping.
- Compile/GLSL errors:
- For live preview (
ShaderView), errors are surfaced through the UI (Snackbar +ErrorListModal). - For manual runs (
PreviewActivity), error info is stored inPreviewActivity.renderStatus.infoLog.
- For live preview (
- Use
adb shell dumpsys meminfo ...ormake meminfoto inspect memory usage;make glxinfodumps gfx stats. - Wallpapers should be tested both with
save_batteryenabled/disabled and while toggling device charging state to verify render mode switching. - Verify notification uniforms by granting the listener permission via the system dialog triggered in
NotificationService.
- Add a new sensor/uniform: extend
ShaderRenderer(new constant, uniform location lookup, value updates), register/unregister a new listener inhardware/, add it toPresetUniformAdapter, and document it inFAQ.md/UI strings. Consider its availability (API level) and gating preferences. - Add shader metadata (e.g., tags): update
DatabaseContract, bump DB version, add columns toShaderDaoCRUD, persist viaShaderManager, show viaShaderAdapter/layouts. - Add a new sample shader: drop a
.glslfile + thumbnail inres/raw/res/drawable, extendSamplesAdapterentry, add translation strings for name/rationale, and bump screenshots if needed. - Modify shader preview behavior: touch
ShaderViewManager,ShaderRenderer, andMainActivity. Remember manual-run flows rely onPreviewActivity, not the embedded GLSurfaceView. - Adjust import/export: modify
ImportExportAsFilesand the preference wiring; keep runtime permissions + scoped storage differences in mind. - Extend preferences: add entries to
res/xml/preferences.xml, define keys/defaults inPreferences, updatePreferencesFragmentsummaries and any code paths referencing the new setting. - Release prep: update
CHANGELOG.md,fastlane/metadata/**/changelogs, bumpversionCode/versionNameinapp/build.gradle.kts, runmake releaseormake bundle, and refresh screenshots if UI changed.
README.md– feature overview, download links, support info.FAQ.md– answers referenced from settings (NavigationManager → FAQ).PRIVACY.md– sensors/camera/mic usage disclosure (needed for stores).CONTRIBUTING.md– workflow, formatting rules, EditorConfig hints.fastlane/metadata/**– store copy/screenshots; keep translations consistent withres/values-*.
- When adding string resources/translations, always add them for all supported languages.
- Prefer
ShaderEditorApp.preferencesfor all preference reads/writes to keep state centralized. - Respect the async loading patterns: DAO operations should not touch the UI thread; use
Executors+Handlerthe same way existing code does. - Mind multi-window:
PreviewActivityis markedsingleInstanceand may run adjacent toMainActivityifRUN_MODEis set accordingly. - Keep GLSL sources ASCII-only to avoid compiler crashes (
ShaderView.removeNonAscii()already enforces this). - When touching layout/system bar behavior, update
SystemBarMetricsto maintain consistent inset handling. - For wallpaper-specific bugs, remember the engine uses
GLSurfaceView.RENDERMODE_WHEN_DIRTYwhen on low battery; test both render modes. - Always recycle/release bitmaps and textures (see
TextureViewFragmentandBitmapEditor) to avoid spikes.
With this map you should be able to navigate the project quickly, understand the impact of a change, and know where to plug new functionality in. Refer back whenever you need to chase a flow—from GLSL compilation, to sensor wiring, to persistence, to the UI. Happy hacking!