Skip to content

ADFA-3694: add day/night icon support for plugins#1213

Merged
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-3694
Apr 21, 2026
Merged

ADFA-3694: add day/night icon support for plugins#1213
Daniel-ADFA merged 4 commits into
stagefrom
ADFA-3694

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

Plugins can now declare plugin.icon_day and plugin.icon_night in their AndroidManifest meta-data, pointing at asset paths inside the APK. On install, PluginLoader extracts both icons into a per-plugin icons directory and exposes them via PluginInfo, so PluginListAdapter renders the correct asset for the current system theme.

Debug plugins must declare both icons, validated during install via a new getPluginValidation API. The fallback ic_extension drawable is retinted with colorOnSurface so it adapts to light and dark themes. Also sets an explicit compileSdk on plugin-api so standalone plugin builds configure correctly.

Screen_recording_20260418_224256.webm

  Plugins can now declare plugin.icon_day and plugin.icon_night in their
  AndroidManifest meta-data, pointing at asset paths inside the APK. On
  install, PluginLoader extracts both icons into a per-plugin icons
  directory and exposes them via PluginInfo, so PluginListAdapter renders
  the correct asset for the current system theme. Debug plugins must
  declare both icons, validated during install via a new
  getPluginValidation API.

  The fallback ic_extension drawable is retinted with colorOnSurface so
  it adapts to light and dark themes. Also sets an explicit compileSdk on
  plugin-api so standalone plugin builds configure correctly.
@Daniel-ADFA Daniel-ADFA requested a review from a team April 18, 2026 21:48
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Release Notes - Day/Night Icon Support for Plugins

Features

  • Plugins can now declare day/night icon assets via plugin.icon_day and plugin.icon_night metadata keys in AndroidManifest, pointing to asset paths within the APK
  • Icons are automatically extracted during plugin installation into a per-plugin icons directory and exposed via PluginInfo
  • PluginListAdapter selects and renders the appropriate icon based on the current system UI theme (dark or light mode)
  • Fallback drawable ic_extension has been updated to use the theme-aware colorOnSurface attribute instead of hardcoded black, ensuring it adapts to both light and dark themes
  • Plugin metadata now includes optional iconDayPath and iconNightPath properties to reference extracted icon locations

Install-Time Validation

  • Debug plugins are now required to declare both day and night icons; this requirement is validated during installation via a new getPluginValidation() API
  • Installation will fail with a clear error message if a debug plugin is missing either required icon declaration
  • Non-debug plugins can optionally declare icons; missing icons degrade gracefully to the fallback drawable

Developer Changes

  • plugin-api module now has an explicit compileSdk configuration to ensure standalone plugin builds are configured correctly
  • New public API in PluginManager: getPluginValidation(pluginFile: File) returns validation metadata including debug status and icon entry existence checks

Implementation Details

  • Icon extraction includes path traversal protection to prevent malicious APKs from writing outside the designated plugin icon directory
  • Icon extraction failures are logged as warnings but do not prevent plugin loading, allowing backward compatibility with plugins that don't provide icons
  • Extracted icon cache is cleaned up when plugins are uninstalled

Walkthrough

Adds theme-aware plugin icon support: manifest fields, extraction into app-private cache, validation API, and adapter loading logic choosing day/night icon paths with drawable fallbacks; installer enforces icon presence for debug plugins and deletes invalid uploads.

Changes

Cohort / File(s) Summary
UI Adapter & Drawable
app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt, app/src/main/res/drawable/ic_extension.xml
Adapter chooses iconNightPath vs iconDayPath based on system dark mode, clears existing ImageView styling, loads icon files via Glide with R.drawable.ic_extension fallback; drawable color switched to ?attr/colorOnSurface.
Plugin repository / install flow
app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
installPluginFromFile now uses getPluginValidation(), deletes invalid files on failure, enforces presence of icon_day/icon_night entries for debug plugins and throws on missing entries before installing.
PluginManager core & metadata mapping
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
Added getPluginValidation() and PluginValidation type, refactored loading via loadAndValidate(), integrated icon extraction into loading, extended LoadedPlugin and getAllPlugins() to include extracted icon paths, and clean up icon cache on uninstall.
Plugin loader: APK inspection & extraction
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
Added isDebuggable(), hasEntry(...), and extractPluginIcons(...) to read APK debuggable flag, check entry existence, and extract icon_day/icon_night entries into per-plugin plugin_icons/<id> directory; PluginManifest now includes iconDay/iconNight fields mapped from manifest meta-data.
Plugin API metadata
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.kt
PluginMetadata gains optional properties iconDayPath: String? and iconNightPath: String? to carry extracted icon file paths into consumers.

Sequence Diagram(s)

sequenceDiagram
    participant Repo as PluginRepositoryImpl
    participant PM as PluginManager
    participant PL as PluginLoader
    participant FS as FileSystem

    Repo->>PM: getPluginValidation(pluginFile)
    PM->>PM: loadAndValidate(pluginFile)
    PM->>PL: new PluginLoader(pluginFile)
    PL->>FS: read APK (manifest + entries)
    FS-->>PL: PluginManifest + ZIP entries
    PL->>PL: isDebuggable(), hasEntry(icon_day/icon_night)
    PL-->>PM: PluginValidation(manifest, isDebug, iconDayExists, iconNightExists)
    PM-->>Repo: Result<PluginValidation>

    alt Validation Success
        Repo->>FS: copy plugin to pluginsDir (.cgp/.apk)
        Repo->>PM: loadPlugins()
        PM->>PL: extractPluginIcons(manifest.id, manifest)
        PL->>FS: write plugin_icons/<id>/icon_day*, icon_night*
        FS-->>PL: extracted paths
    else Validation Failure
        Repo->>FS: delete uploaded pluginFile
        Repo-->>Repo: throw exception
    end
Loading
sequenceDiagram
    participant Adapter as PluginListAdapter
    participant System as SystemUI
    participant FS as FileSystem
    participant Glide as Glide

    Adapter->>System: isSystemInDarkMode()
    System-->>Adapter: isDark (true/false)

    alt Dark
        Adapter->>FS: resolve `plugin.metadata.iconNightPath`
    else Light
        Adapter->>FS: resolve `plugin.metadata.iconDayPath`
    end

    FS-->>Adapter: file exists? (yes/no)
    alt exists
        Adapter->>Glide: load(file) with placeholder/error `R.drawable.ic_extension`
        Glide-->>Adapter: bitmap drawable set
    else not exists
        Adapter->>Glide: Glide.clear(pluginIcon)
        Adapter->>Adapter: pluginIcon.setImageResource(R.drawable.ic_extension)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa
  • jomen-adfa
  • jatezzz

Poem

🐰 I nibble manifests by lamplight and sun,
Extracting icons for plugins, each one,
Night or day I tuck them in a cache,
So UI sprites wear the proper sash. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly and concisely describes the main feature: adding day/night icon support for plugins, which aligns with the core changeset across all modified files.
Description check ✅ Passed The PR description comprehensively covers the implementation details, user-facing functionality, and related changes like icon validation for debug plugins and theme-aware fallback icons.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-3694

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt (1)

182-190: Populate the pluginPackageInfo cache on the fallback path.

When pluginPackageInfo is null, this method calls getPackageArchiveInfo(...) but does not assign the result back to the field, so a subsequent call (or getPluginMetadata() invoked later) re-parses the archive. Minor, but trivially fixable:

♻️ Proposed refactor
-        val packageInfo = pluginPackageInfo
-            ?: context.packageManager.getPackageArchiveInfo(
-                pluginApk.absolutePath,
-                PackageManager.GET_META_DATA
-            ) ?: return false
+        val packageInfo = pluginPackageInfo ?: context.packageManager.getPackageArchiveInfo(
+            pluginApk.absolutePath,
+            PackageManager.GET_META_DATA
+        )?.also { pluginPackageInfo = it } ?: return false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`
around lines 182 - 190, The isDebuggable() method currently calls
context.packageManager.getPackageArchiveInfo(...) when pluginPackageInfo is null
but doesn't cache that result; update isDebuggable() so that when
pluginPackageInfo is null you assign the returned PackageInfo to the
pluginPackageInfo field (e.g., pluginPackageInfo =
context.packageManager.getPackageArchiveInfo(...)) before using it to compute
appInfo and the debuggable flag, ensuring subsequent callers (and
getPluginMetadata()) reuse the parsed PackageInfo.
app/src/main/res/drawable/ic_extension.xml (1)

7-9: Theme attribute requires a themed context at inflation.

?attr/colorOnSurface is resolved against the inflating Context's theme. In PluginListAdapter the fallback is applied via pluginIcon.setBackgroundResource(R.drawable.ic_extension) — the theme will resolve through the ImageView's context, which is fine here. Just note that if this drawable is ever inflated from a non-Material/non-themed context (e.g., a raw Application context), colorOnSurface will not resolve and you'll get a crash/runtime error. Consider ?attr/colorControlNormal or a fallback via <selector> if broader reuse is anticipated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/drawable/ic_extension.xml` around lines 7 - 9, The vector
drawable ic_extension uses the themed attribute ?attr/colorOnSurface which will
crash if inflated with a non-Material/non-themed Context; update the drawable or
its usage so it always resolves: either switch the fill attribute to a
broader-safe attribute like ?attr/colorControlNormal, or provide a fallback
tint/selector in the drawable, and ensure call sites such as PluginListAdapter
where pluginIcon.setBackgroundResource(R.drawable.ic_extension) keep using a
themed Context (or use ImageView#setImageResource with a themed wrapper). Locate
ic_extension and PluginListAdapter/pluginIcon.setBackgroundResource and
implement one of these fixes so the attribute always resolves.
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt (1)

349-354: Consider logging when icons were expected but extraction returned null.

extractPluginIcons already catches exceptions internally and returns null to null with a warning. But it also returns null to null silently if the manifest declared an iconDay/iconNight whose zip entry was not found (zip.getEntry(entryPath) == null). That no-entry case won't surface in logs, making it hard to debug why a plugin falls back to ic_extension. Consider a debug log here when manifest.iconDay != null && iconDayPath == null (and same for night).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`
around lines 349 - 354, The current extraction swallow of missing zip entries
makes it silent when icons fallback to ic_extension; after calling
pluginLoader.extractPluginIcons in PluginManager (the destructuring into
iconDayPath and iconNightPath), add conditional debug logs using the existing
logger when a manifest requested an icon but got null: if manifest.iconDay !=
null && iconDayPath == null log a debug indicating the day icon entry was not
found for manifest.id and similarly for manifest.iconNight != null &&
iconNightPath == null for the night icon; place these checks immediately after
the extractPluginIcons call so missing-entry cases are visible while preserving
existing exception logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt`:
- Around line 76-79: The fallback branch currently uses
pluginIcon.setBackgroundResource(R.drawable.ic_extension) which places the icon
in the background (losing scaleType, padding, tint and any foreground
behaviour); change it to set the drawable on the ImageView foreground like the
success path by calling pluginIcon.setImageResource(R.drawable.ic_extension) and
also ensure you clear/reset the background and image tint the same way the
success branch does (mirror the success-branch calls that clear background and
imageTintList) so the fallback renders consistently.
- Around line 62-79: In PluginListAdapter.bind(), avoid synchronous
BitmapFactory.decodeFile and File.exists on the main thread (currently using
iconDayPath/iconNightPath and BitmapFactory.decodeFile); instead load icons
asynchronously and cache them: either integrate an image loader (Coil/Glide) to
load plugin.metadata.iconDayPath / iconNightPath into pluginIcon with a
placeholder (and clear previous request), or implement a background
coroutine/Dispatcher.IO loader that checks file existence off the main thread,
decodes with BitmapFactory.Options (inSampleSize) and stores results in an
LruCache keyed by "${plugin.id}:${isNight}". Ensure you set the
placeholder/default drawable on the main thread immediately, switch to the
decoded bitmap on the main thread, and avoid allocating/decoding in bind()
synchronously.

In
`@app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt`:
- Around line 87-96: The current debug-only check in PluginRepositoryImpl only
ensures manifest declares metadata.iconDay/iconNight but not that those asset
paths actually exist in the APK; update the install/validation flow (where
validation.isDebug is checked and before keeping the plugin) to also verify the
assets can be extracted — either call PluginLoader.extractPluginIcons and assert
it returns non-null paths for both day and night, or open the plugin APK ZipFile
and call getEntry(metadata.iconDay) and getEntry(metadata.iconNight) and ensure
they are non-null; if either extraction/entry check fails, delete the pluginFile
and throw the same IllegalArgumentException (or a clear variant) indicating the
missing asset(s) so debug plugins must include real icon files.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`:
- Around line 192-215: The extractPluginIcons function currently resolves
extracted files by basename which allows collisions; change it to (1) clear
iconDir contents before extraction, (2) when writing manifest.iconDay and
manifest.iconNight use deterministic role-based filenames (e.g.,
"icon_day.<ext>" and "icon_night.<ext>") derived from the original entry
extension instead of entryPath.substringAfterLast('/'), and (3) preserve the
existing path normalization/security check against targetPath and still return
the absolute paths; update the inner extractEntry helper and the callsites that
build outPath to use these role-based names and keep
PluginManifest/iconDir/targetPath references to locate the code to change.

---

Nitpick comments:
In `@app/src/main/res/drawable/ic_extension.xml`:
- Around line 7-9: The vector drawable ic_extension uses the themed attribute
?attr/colorOnSurface which will crash if inflated with a non-Material/non-themed
Context; update the drawable or its usage so it always resolves: either switch
the fill attribute to a broader-safe attribute like ?attr/colorControlNormal, or
provide a fallback tint/selector in the drawable, and ensure call sites such as
PluginListAdapter where
pluginIcon.setBackgroundResource(R.drawable.ic_extension) keep using a themed
Context (or use ImageView#setImageResource with a themed wrapper). Locate
ic_extension and PluginListAdapter/pluginIcon.setBackgroundResource and
implement one of these fixes so the attribute always resolves.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 349-354: The current extraction swallow of missing zip entries
makes it silent when icons fallback to ic_extension; after calling
pluginLoader.extractPluginIcons in PluginManager (the destructuring into
iconDayPath and iconNightPath), add conditional debug logs using the existing
logger when a manifest requested an icon but got null: if manifest.iconDay !=
null && iconDayPath == null log a debug indicating the day icon entry was not
found for manifest.id and similarly for manifest.iconNight != null &&
iconNightPath == null for the night icon; place these checks immediately after
the extractPluginIcons call so missing-entry cases are visible while preserving
existing exception logging.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`:
- Around line 182-190: The isDebuggable() method currently calls
context.packageManager.getPackageArchiveInfo(...) when pluginPackageInfo is null
but doesn't cache that result; update isDebuggable() so that when
pluginPackageInfo is null you assign the returned PackageInfo to the
pluginPackageInfo field (e.g., pluginPackageInfo =
context.packageManager.getPackageArchiveInfo(...)) before using it to compute
appInfo and the debuggable flag, ensuring subsequent callers (and
getPluginMetadata()) reuse the parsed PackageInfo.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac8fa8e6-c918-45e7-a61f-fcb710e969ae

📥 Commits

Reviewing files that changed from the base of the PR and between 9af9102 and bbdd4fc.

📒 Files selected for processing (7)
  • app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
  • app/src/main/res/drawable/ic_extension.xml
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt

Comment thread app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 1203-1205: The cleanup code in PluginManager.kt uses pluginId
directly when constructing deletion targets (the
File(context.getDir("plugin_icons", ...), pluginId) call and the similar calls
for "plugin_native_libs" and pluginsDir), allowing path traversal; fix by
resolving and validating the target directory is contained within the intended
root before deleting: create the root File (e.g., context.getDir("plugin_icons",
...)), build the candidate File via File(root, pluginId), obtain canonicalFile
(or toPath().normalize()) for both root and candidate and ensure
candidateCanonical.path startsWith(rootCanonical.path) (or
candidatePath.normalize().startsWith(rootPath.normalize()) ) and only then call
deleteRecursively(); apply the same containment check to the deletion logic for
plugin_native_libs and the pluginsDir cleanup (mirroring the containment
approach used in PluginLoader.extractNativeLibs() and
PluginLoader.extractPluginIcons()) to prevent escaping the root.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`:
- Around line 192-213: The hasEntry/ extractPluginIcons flow is vulnerable to
directory entries and path traversal: update hasEntry to reject ZipEntry objects
where entry.isDirectory is true; in extractPluginIcons validate and sanitize the
manifest-controlled pluginId (reject empty, segments like .. or leading /)
before constructing iconDir, and perform all path resolution and security checks
(resolve and normalize targetPath and each prospective outPath) before calling
iconDir.deleteRecursively(); inside the nested extractEntry function explicitly
return null for directory entries (zip.getEntry(...).isDirectory) and ensure
outPath.startsWith(targetPath) is checked prior to any deletion or file creation
to prevent traversal attacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e430183-7784-47f1-9d52-aecd23ac53e5

📥 Commits

Reviewing files that changed from the base of the PR and between bbdd4fc and fb03d07.

📒 Files selected for processing (4)
  • app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants