ADFA-3694: add day/night icon support for plugins#1213
Conversation
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.
📝 WalkthroughRelease Notes - Day/Night Icon Support for PluginsFeatures
Install-Time Validation
Developer Changes
Implementation Details
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt (1)
182-190: Populate thepluginPackageInfocache on the fallback path.When
pluginPackageInfois null, this method callsgetPackageArchiveInfo(...)but does not assign the result back to the field, so a subsequent call (orgetPluginMetadata()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/colorOnSurfaceis resolved against the inflatingContext's theme. InPluginListAdapterthe fallback is applied viapluginIcon.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 rawApplicationcontext),colorOnSurfacewill not resolve and you'll get a crash/runtime error. Consider?attr/colorControlNormalor 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.
extractPluginIconsalready catches exceptions internally and returnsnull to nullwith a warning. But it also returnsnull to nullsilently if the manifest declared aniconDay/iconNightwhose 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 toic_extension. Consider a debug log here whenmanifest.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
📒 Files selected for processing (7)
app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.ktapp/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.ktapp/src/main/res/drawable/ic_extension.xmlplugin-api/src/main/kotlin/com/itsaky/androidide/plugins/IPlugin.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.ktapp/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt
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