Skip to content

Commit fee3efb

Browse files
yaturnerclaude
andcommitted
ADFA-5444: Skip the Downloads restore/collision check for plugin templates
Uninstalling a plugin-provided template (e.g. from the Flutter plugin) unconditionally tried to "restore" it to Downloads, tripping a false "already exists" collision whenever an unrelated file happened to share its name there - even though the template was extracted straight from the plugin's own bundled resources and never touched Downloads in the first place. A plugin-provenance item now just gets deleted from the templates directory on uninstall, matching how PluginProjectManager and IdeTemplateServiceImpl already remove these templates elsewhere. User-imported templates keep the existing restore-to-Downloads behavior, since those genuinely came from there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 5a26dfa commit fee3efb

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

‎app/src/main/java/com/itsaky/androidide/repositories/TemplateRepository.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ interface TemplateRepository {
2020
/** Moves [item]'s file from Downloads into the templates directory and reloads templates. */
2121
suspend fun installTemplate(item: CgtFileItem): Result<Unit>
2222

23-
/** Restores a copy of [item]'s file to Downloads, removes it from the templates directory, and reloads templates. */
23+
/**
24+
* Removes [item] from the templates directory and reloads templates. A user-imported item is
25+
* restored to Downloads first; a plugin-provided item is just deleted, since it never came
26+
* from Downloads in the first place.
27+
*/
2428
suspend fun uninstallTemplate(item: CgtFileItem): Result<Unit>
2529

2630
/** Deletes a not-installed [item]'s file from Downloads. */

‎app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ class TemplateRepositoryImpl(
143143
check(item.installed) { "'${item.name}' is not installed" }
144144
check(item.provenance != TemplateProvenance.BUNDLED) { "Cannot uninstall the bundled template" }
145145

146+
if (item.provenance == TemplateProvenance.PLUGIN) {
147+
// Plugin templates are extracted straight from the plugin's own bundled
148+
// resources (PluginProjectManager.extractBundledCgtTemplates) - they never
149+
// passed through Downloads, so there's nothing to restore there and no
150+
// collision to check. Uninstalling one just deletes the extracted copy,
151+
// matching how PluginProjectManager/IdeTemplateServiceImpl already remove
152+
// these elsewhere (cleanupPluginTemplates/unregisterTemplate).
153+
if (!item.file.delete()) {
154+
throw IOException("Failed to delete ${item.file.absolutePath}")
155+
}
156+
ITemplateProvider.getInstance(reload = true)
157+
return@withContext Result.success(Unit)
158+
}
159+
146160
// Restore a copy to Downloads BEFORE removing it from the store: if the restore
147161
// throws, the store copy below is never touched, so the user's only copy survives.
148162
val restored = File(downloadDir, item.file.name)

‎app/src/test/java/com/itsaky/androidide/repositories/TemplateRepositoryImplTest.kt‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,35 @@ class TemplateRepositoryImplTest {
111111
assertThat(existingDownload.readText()).isEqualTo("already in downloads")
112112
}
113113

114+
@Test
115+
fun uninstallTemplate_pluginProvenance_neverChecksOrTouchesDownloads() =
116+
runTest {
117+
val source = File(templatesDir, "plugin_flutter_activity.cgt").apply { writeText("installed") }
118+
val pluginItem =
119+
CgtFileItem(
120+
file = source,
121+
name = source.name,
122+
templates = listOf(TemplateMetadata("T", "d", "1.0")),
123+
installed = true,
124+
provenance = TemplateProvenance.PLUGIN,
125+
)
126+
// A same-named file already sitting in Downloads must not trip a collision check for
127+
// a plugin-provided template (ADFA-5444): it never came from Downloads, so there's
128+
// nothing to restore there. templatesDir is made read-only so the only way to reach
129+
// this specific IOException is the plugin branch's own delete - the old
130+
// restore-to-Downloads path would instead fail earlier with an IllegalStateException
131+
// from the (wrongly reached) "already exists" check.
132+
val existingDownload = File(downloadDir, "plugin_flutter_activity.cgt").apply { writeText("unrelated") }
133+
check(templatesDir.setWritable(false)) { "test setup: could not make templatesDir read-only" }
134+
135+
val result = repository.uninstallTemplate(pluginItem)
136+
137+
assertThat(result.isFailure).isTrue()
138+
assertThat(result.exceptionOrNull()).isInstanceOf(IOException::class.java)
139+
assertThat(source.exists()).isTrue()
140+
assertThat(existingDownload.readText()).isEqualTo("unrelated")
141+
}
142+
114143
@Test
115144
fun uninstallTemplate_deleteFails_rollsBackAndLeavesExactlyOneCopy() =
116145
runTest {

0 commit comments

Comments
 (0)