Skip to content

ADFA-3695: Implement plugin conflict detection with signature verification and overwrite confirmation#1200

Merged
Daniel-ADFA merged 4 commits intostagefrom
ADFA-3695
Apr 21, 2026
Merged

ADFA-3695: Implement plugin conflict detection with signature verification and overwrite confirmation#1200
Daniel-ADFA merged 4 commits intostagefrom
ADFA-3695

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

@Daniel-ADFA Daniel-ADFA requested review from a team and jatezzz April 17, 2026 13:20
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Release Notes: Plugin Conflict Detection with Signature Verification

Features Added

  • Plugin Conflict Detection: Automatically detects when a new plugin with the same ID as an already-installed plugin is being installed
  • Signature Verification: Compares signing certificates of incoming and existing plugins to determine if they are from the same source
  • User-Friendly Overwrite Confirmation: Displays a confirmation dialog showing existing and incoming plugin details when signatures match, allowing users to replace the plugin
  • Improved Error Handling: Clear error messages for signature mismatches, invalid plugin files, and other installation failures
  • Better Error UX: Enhanced flashbar notifications with "Copy" action for detailed error information when needed

Technical Changes

  • Added two new PluginRepository methods: getPluginMetadataFromFile() and haveMatchingSignatures()
  • Extended PluginManager with haveMatchingSignatures() to compare plugin signatures
  • New PluginLoader.getSignatureHash() method extracts APK signing certificates using both modern and legacy Android APIs
  • Extended PluginManifest with toPluginMetadata() converter for consistent metadata handling
  • New UI events and effects: PluginManagerUiEvent.ConfirmOverwrite and PluginManagerUiEffect.ShowOverwriteConfirmation
  • Added new string resources for localized messages related to plugin conflicts and errors

Risk Factors & Best Practice Violations

⚠️ Medium Risk - API Compatibility: The PluginLoader.loadPluginClasses() method changed from returning nullable DexClassLoader? to non-null DexClassLoader. While exception handling was improved, callers must verify they properly handle the new behavior.

⚠️ Low Risk - Deprecated Android APIs: Code uses deprecated PackageManager.GET_SIGNATURES flag alongside modern GET_SIGNING_CERTIFICATES. Though properly suppressed, this requires API level support across versions. Signature extraction falls back gracefully to legacy APIs when needed.

⚠️ Low Risk - Performance Consideration: Signature comparison creates two separate PluginLoader instances to load and extract signatures. For large plugin files, this could impact performance slightly but is a one-time operation during installation.

⚠️ Low Risk - Early Exit Pattern: Installation flow exits early on conflict detection, placing state cleanup in finally block to ensure proper cleanup. Verify existing error reporting works correctly with the new early-exit path.

Breaking Changes

  • PluginLoader.loadPluginClasses() now returns non-null DexClassLoader instead of nullable type; exceptions are thrown instead of returning null

Testing Recommendations

  • Verify signature matching works correctly with plugins signed by the same and different certificates
  • Test the overwrite confirmation dialog with various plugin combinations
  • Validate error messages display correctly for invalid and corrupted plugin files
  • Test installation state cleanup when conflicts are detected

Walkthrough

Adds an overwrite-confirmation flow for plugin installs: incoming plugin metadata and signatures are read from the file; if an installed plugin with the same ID exists, signatures are compared and either an error is emitted for mismatch or a Replace/Cancel dialog is shown for matching signatures.

Changes

Cohort / File(s) Summary
Activity / UI
app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
Updated ShowError handling (conditional lifetime, copy-to-clipboard action) and added ShowOverwriteConfirmation dialog with Replace/Cancel that dispatches ConfirmOverwrite on Replace.
ViewModel / UI State
app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt, app/src/main/java/com/itsaky/androidide/ui/models/PluginManagerUiState.kt
Added ConfirmOverwrite event and ShowOverwriteConfirmation effect; installPlugin gained checkConflict flag and new conflict-resolution flow (read metadata, detect existing plugin, check signatures, emit error or show overwrite confirmation). Reset logic moved into finally block.
Repository Layer
app/src/main/java/com/itsaky/androidide/repositories/PluginRepository.kt, app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
Added suspend fun getPluginMetadataFromFile(File): Result<PluginMetadata> and suspend fun haveMatchingSignatures(File, String): Result<Boolean> implemented via plugin-manager delegation on IO dispatcher.
Plugin Manager Core
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
Added haveMatchingSignatures(incomingFile, existingPluginId): Boolean; adjusted loadPlugin error messaging and changed plugin metadata assembly to use toPluginMetadata().
Plugin Loaders & Manifest
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
loadPluginClasses now returns non-null DexClassLoader or throws; added getSignatureHash() to extract signer bytes; added PluginManifest.toPluginMetadata() and improved JSON parse warnings.
Resources / Strings
resources/src/main/res/values/strings.xml
Added strings for "already installed" title, overwrite confirmation message, signature mismatch message, plugin error clip label, and invalid/corrupted plugin file message.

Sequence Diagram

sequenceDiagram
    participant User
    participant Activity as PluginManagerActivity
    participant ViewModel as PluginManagerViewModel
    participant Repo as PluginRepository
    participant Manager as PluginManager

    User->>Activity: Request install (uri)
    Activity->>ViewModel: OnEvent(InstallPlugin(uri))
    ViewModel->>Repo: download URI -> tempFile
    ViewModel->>Repo: getPluginMetadataFromFile(tempFile)
    Repo->>Manager: getPluginMetadataOnly(tempFile)
    Manager-->>Repo: PluginMetadata
    Repo-->>ViewModel: Result<PluginMetadata>

    alt Installed plugin with same ID exists
        ViewModel->>Repo: haveMatchingSignatures(tempFile, existingId)
        Repo->>Manager: haveMatchingSignatures(...)
        Manager-->>Repo: Boolean
        Repo-->>ViewModel: Result<Boolean>

        alt Signatures mismatch
            ViewModel-->>Activity: ShowError(msg_plugin_signature_mismatch)
            Activity->>User: Display error
        else Signatures match
            ViewModel-->>Activity: ShowOverwriteConfirmation(existing, incoming)
            Activity->>User: Show Replace/Cancel dialog
            User->>Activity: Tap Replace
            Activity->>ViewModel: OnEvent(ConfirmOverwrite(uri, deleteAfter))
            ViewModel->>Repo: installPluginFromFile(tempFile, checkConflict=false)
            Repo->>Manager: installPlugin(...)
            Manager-->>Repo: Success
            Repo-->>ViewModel: Result<Unit>
            ViewModel-->>Activity: ShowSuccess
            Activity->>User: Display success
        end
    else No existing plugin
        ViewModel->>Repo: installPluginFromFile(tempFile)
        Repo->>Manager: installPlugin(...)
        Manager-->>Repo: Success
        Repo-->>ViewModel: Result<Unit>
        ViewModel-->>Activity: ShowSuccess
        Activity->>User: Display success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa
  • dara-abijo-adfa
  • jatezzz

Poem

🐰 I sniffed a plugin, small and spry,
I read its metadata with a curious eye.
If hashes match, I offer Replace so neat,
If not, I thump and sound retreat.
Hop, dialog, copy—then install complete. 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is empty (placeholder content), providing no information about the changeset. Add a meaningful description explaining the plugin conflict detection feature, signature verification logic, and user-facing overwrite confirmation flow.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main objective of the changeset: implementing plugin conflict detection with signature verification and overwrite confirmation, which aligns with all the key changes across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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-3695

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: 6

🧹 Nitpick comments (1)
app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt (1)

208-225: Copy action only exposed for parameterized errors — verify this matches intent.

The Copy action and indefinite duration are only attached when effect.formatArgs.isNotEmpty(). Several error strings used in this flow (e.g. msg_plugin_file_not_found, msg_plugin_signature_mismatch does take args, msg_plugin_install_failed does too) have varying parameter counts, so "has formatArgs" is being used as a proxy for "is a long/detailed error, allow copy and hold open indefinitely." That coupling is implicit and fragile — a future error with no args could still benefit from Copy, and a short arg-based error will now never auto-dismiss. Consider making this an explicit flag on ShowError (e.g. isCopyable: Boolean or autoDismiss: Boolean) rather than deriving it from argument presence.

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

In `@app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt`
around lines 208 - 225, The current logic in handleUiEffect uses
effect.formatArgs.isNotEmpty() to decide both the flashbar duration and whether
to show the Copy action, which couples formatting arguments to UI behavior;
update the ShowError model (PluginManagerUiEffect.ShowError) to include explicit
flags (e.g. isCopyable: Boolean and/or autoDismiss: Boolean), then change
handleUiEffect to use those flags instead of effect.formatArgs.isNotEmpty() when
configuring builder (duration, positiveActionText, positiveActionTapListener and
builder.showOnUiThread), leaving formatArgs only for string formatting.
🤖 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/viewmodels/PluginManagerViewModel.kt`:
- Around line 264-266: The code silently skips conflict/signature checks when
getPluginMetadataFromFile(tempFile).getOrNull() returns null; update
PluginManagerViewModel so that if incoming is null you log a warning (include
tempFile and parsing error if available) and abort with an explicit user-facing
error (or set a UI state error) instead of proceeding to installPluginFromFile;
if you must continue, at minimum record in _uiState why conflict detection was
skipped and ensure existing overwrite/signature guards are not bypassed. Use the
same call sites around incoming/existing to add the logging and error-path
handling so malformed archives cannot circumvent overwrite checks.
- Around line 268-291: The signature check currently treats verification errors
as "match" because haveMatchingSignatures(...).getOrDefault(true) defaults to
true; change this to fail closed by treating any non-success as mismatch (use
getOrDefault(false) or explicitly handle the Result/Optional error case) inside
PluginManagerViewModel where signaturesMatch is computed; ensure tempFile is
still deleted, and replace the branch so that verification failures route to
_uiEffect.trySend(PluginManagerUiEffect.ShowError(...)) (or a distinct "could
not verify signature" error) instead of showing
PluginManagerUiEffect.ShowOverwriteConfirmation for unverified plugins.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 525-531: The haveMatchingSignatures method currently treats null
signature extraction as a match; change this to fail-closed by treating any null
incomingSig or existingSig as a mismatch (return false or surface an error) so a
missing signature from PluginLoader.getSignatureHash does not allow overwrite;
update the logic in haveMatchingSignatures (and add a clear processLogger/error
path if desired) to return false when either signature is null and only return
contentEquals(...) when both are non-null.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt`:
- Around line 111-113: The catch in PluginLoader.loadPluginClasses currently
throws a RuntimeException which makes the declared nullable return type
DexClassLoader? and the null-check in PluginManager.loadPlugin dead code; either
make the return non-nullable and remove the caller's null-check, or preserve the
existing contract by logging the exception and returning null instead of
throwing. Update PluginLoader.loadPluginClasses to catch Exception (e), use the
module logger/processLogger to log the full error (including e and e.message)
with context "Failed to load plugin classes", and then return null so
PluginManager.loadPlugin's if (classLoader == null) branch remains reachable and
correct.
- Around line 251-267: PluginLoader.getSignatureHash currently swallows all
exceptions and returns null, which lets PluginManager.haveMatchingSignatures
treat failures as a match; change getSignatureHash to not fail silently: catch
only expected exceptions if needed, log the exception (use the existing logger)
with context (pluginApk path) and then rethrow or propagate the exception (do
not return null on unexpected errors) so the caller can fail closed; update
caller PluginManager.haveMatchingSignatures to handle the propagated exception
and treat extraction failures as non-matching (or surface the error) instead of
treating null as a match.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt`:
- Around line 110-121: The parseFromString/parseFromJar methods
(PluginManifestParser.parseFromString / parseFromJar) currently declare
PluginManifest? but rethrow exceptions, breaking the fallback in
PluginLoader.getPluginMetadata which expects null-on-failure; restore the
original null-on-failure contract by catching Exception inside
parseFromString/parseFromJar, log the error (include exception details) and
return null on parse errors, or alternatively change both method signatures to
non-null and update all callers (notably PluginLoader.getPluginMetadata and any
getPluginMetadataOnly flow that calls PluginManifestParser.parseFromJar) to
handle the thrown exception — pick one consistent approach and apply it across
the referenced symbols.

---

Nitpick comments:
In `@app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt`:
- Around line 208-225: The current logic in handleUiEffect uses
effect.formatArgs.isNotEmpty() to decide both the flashbar duration and whether
to show the Copy action, which couples formatting arguments to UI behavior;
update the ShowError model (PluginManagerUiEffect.ShowError) to include explicit
flags (e.g. isCopyable: Boolean and/or autoDismiss: Boolean), then change
handleUiEffect to use those flags instead of effect.formatArgs.isNotEmpty() when
configuring builder (duration, positiveActionText, positiveActionTapListener and
builder.showOnUiThread), leaving formatArgs only for string formatting.
🪄 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: 725cbf15-e1f5-4427-91bc-25de5d322909

📥 Commits

Reviewing files that changed from the base of the PR and between 2b853ff and d330e17.

📒 Files selected for processing (9)
  • app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepository.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/ui/models/PluginManagerUiState.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.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
  • resources/src/main/res/values/strings.xml

Comment thread app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.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
`@app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt`:
- Line 276: The conflict check currently reads from the cached UI list via
_uiState.value.plugins (val existing = _uiState.value.plugins.find {
it.metadata.id == incoming.id }), which can be stale; replace that lookup with a
direct repository call (e.g. call pluginRepository.getPluginById(incoming.id) or
equivalent) to fetch the canonical on-disk/plugin store entry before deciding
the conflict/signature branch so the overwrite prompt and signature verification
run reliably even while loadPlugins() is still populating the UI state.
- Around line 263-308: The TOCTOU bug is that the verified tempFile is deleted
before the overwrite confirmation, so installPluginFromFile may later install a
new, unverified copy; to fix, persist and install the exact verified file:
change the overwrite flow to pass the verified temp file reference (e.g., its
path or a FileDescriptor) through
PluginManagerUiEffect.ShowOverwriteConfirmation and the ConfirmOverwrite action
instead of the original Uri, keep tempFile alive until the user confirms, and
have the confirm branch call pluginRepository.installPluginFromFile(using that
same tempFile) rather than re-copying the Uri; alternatively, if you prefer the
simpler change, re-run pluginRepository.haveMatchingSignatures(tempFile,
existing.metadata.id) immediately before calling installPluginFromFile in the
confirm branch to ensure the second copy is verified.
🪄 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: 69ef76d1-9aaf-4900-be8e-0b17d6e950d3

📥 Commits

Reviewing files that changed from the base of the PR and between d330e17 and 99a85f8.

📒 Files selected for processing (5)
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.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
  • resources/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (2)
  • resources/src/main/res/values/strings.xml
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt

Comment thread app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt Outdated
@Daniel-ADFA Daniel-ADFA requested a review from jatezzz April 20, 2026 20:25
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: 1

🤖 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 553-556: haveMatchingSignatures currently assumes the installed
plugin file is named "$existingPluginId.cgp", which can miss plugins that keep
their original filename; update haveMatchingSignatures to locate the installed
.cgp by scanning pluginsDir for files ending with ".cgp", loading each file's
manifest/signature using PluginLoader (same class used for incomingFile) and
selecting the file whose manifest ID equals existingPluginId, then compare
signature hashes between the incomingFile and that resolved file; ensure you use
PluginLoader(context, file).getSignatureHash() and/or manifest ID access
consistently so existingSig is not null when a matching manifest exists.
🪄 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: a26a6281-7b26-46d1-8a64-af02d3053832

📥 Commits

Reviewing files that changed from the base of the PR and between 99a85f8 and 3f32f4b.

📒 Files selected for processing (7)
  • app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.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
  • resources/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginLoader.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt

@Daniel-ADFA Daniel-ADFA merged commit caf726b into stage Apr 21, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-3695 branch April 21, 2026 11:04
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.

2 participants