diff --git a/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepository.kt b/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepository.kt index ee497b0106..c3cf94231a 100644 --- a/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepository.kt +++ b/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepository.kt @@ -20,7 +20,11 @@ interface TemplateRepository { /** Moves [item]'s file from Downloads into the templates directory and reloads templates. */ suspend fun installTemplate(item: CgtFileItem): Result - /** Restores a copy of [item]'s file to Downloads, removes it from the templates directory, and reloads templates. */ + /** + * Removes [item] from the templates directory and reloads templates. A user-imported item is + * restored to Downloads first; a plugin-provided item is just deleted, since it never came + * from Downloads in the first place. + */ suspend fun uninstallTemplate(item: CgtFileItem): Result /** Deletes a not-installed [item]'s file from Downloads. */ diff --git a/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt b/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt index 21662008d1..fa08c4cd6d 100644 --- a/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt +++ b/app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt @@ -143,6 +143,20 @@ class TemplateRepositoryImpl( check(item.installed) { "'${item.name}' is not installed" } check(item.provenance != TemplateProvenance.BUNDLED) { "Cannot uninstall the bundled template" } + if (item.provenance == TemplateProvenance.PLUGIN) { + // Plugin templates are extracted straight from the plugin's own bundled + // resources (PluginProjectManager.extractBundledCgtTemplates) - they never + // passed through Downloads, so there's nothing to restore there and no + // collision to check. Uninstalling one just deletes the extracted copy, + // matching how PluginProjectManager/IdeTemplateServiceImpl already remove + // these elsewhere (cleanupPluginTemplates/unregisterTemplate). + if (!item.file.delete()) { + throw IOException("Failed to delete ${item.file.absolutePath}") + } + ITemplateProvider.getInstance(reload = true) + return@withContext Result.success(Unit) + } + // Restore a copy to Downloads BEFORE removing it from the store: if the restore // throws, the store copy below is never touched, so the user's only copy survives. val restored = File(downloadDir, item.file.name) diff --git a/app/src/test/java/com/itsaky/androidide/repositories/TemplateRepositoryImplTest.kt b/app/src/test/java/com/itsaky/androidide/repositories/TemplateRepositoryImplTest.kt index c1c0048630..3fb13d84c8 100644 --- a/app/src/test/java/com/itsaky/androidide/repositories/TemplateRepositoryImplTest.kt +++ b/app/src/test/java/com/itsaky/androidide/repositories/TemplateRepositoryImplTest.kt @@ -111,6 +111,35 @@ class TemplateRepositoryImplTest { assertThat(existingDownload.readText()).isEqualTo("already in downloads") } + @Test + fun uninstallTemplate_pluginProvenance_neverChecksOrTouchesDownloads() = + runTest { + val source = File(templatesDir, "plugin_flutter_activity.cgt").apply { writeText("installed") } + val pluginItem = + CgtFileItem( + file = source, + name = source.name, + templates = listOf(TemplateMetadata("T", "d", "1.0")), + installed = true, + provenance = TemplateProvenance.PLUGIN, + ) + // A same-named file already sitting in Downloads must not trip a collision check for + // a plugin-provided template (ADFA-5444): it never came from Downloads, so there's + // nothing to restore there. templatesDir is made read-only so the only way to reach + // this specific IOException is the plugin branch's own delete - the old + // restore-to-Downloads path would instead fail earlier with an IllegalStateException + // from the (wrongly reached) "already exists" check. + val existingDownload = File(downloadDir, "plugin_flutter_activity.cgt").apply { writeText("unrelated") } + check(templatesDir.setWritable(false)) { "test setup: could not make templatesDir read-only" } + + val result = repository.uninstallTemplate(pluginItem) + + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IOException::class.java) + assertThat(source.exists()).isTrue() + assertThat(existingDownload.readText()).isEqualTo("unrelated") + } + @Test fun uninstallTemplate_deleteFails_rollsBackAndLeavesExactlyOneCopy() = runTest {