Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class TemplateRepositoryImpl(
* Names are compared case-insensitively, matching the stricter of the two install paths
* (`TemplateCollectionRepository.findExistingCollision`), so any row still listed as
* "not installed" is one the user can actually install.
*
* The bundled `core.cgt` is excluded outright (ADFA-5645): it ships with the IDE, isn't
* something the user installed or can uninstall, and has nothing to do with this screen's
* install/uninstall/delete flows.
*/
private fun scanTemplates(): List<CgtFileItem> {
val installed = cgtFilesIn(templatesDir).mapNotNull { file -> parseCgtFile(file, installed = true) }
Expand All @@ -64,9 +68,14 @@ class TemplateRepositoryImpl(
cgtFilesIn(downloadDir)
.filterNot { file -> file.name.lowercase() in installedNames }
.mapNotNull { file -> parseCgtFile(file, installed = false) }
return installed + downloaded
// Filtered out of the combined list, not just `installed`: core.cgt must be excluded even
// if templatesDir doesn't have it yet (bootstrap incomplete) and only a Downloads copy exists.
return (installed + downloaded).filterNot { item -> isCoreArchive(item.name) }
}

/** Case-insensitive, like the twin dedup above and `findExistingCollision`'s reserved name. */
private fun isCoreArchive(fileName: String): Boolean = fileName.equals(TEMPLATE_CORE_ARCHIVE, ignoreCase = true)

private fun cgtFilesIn(dir: File): List<File> =
dir
.listFiles { file -> file.isFile && file.extension.equals(CGT_EXTENSION, ignoreCase = true) }
Expand Down Expand Up @@ -107,11 +116,7 @@ class TemplateRepositoryImpl(
}

private fun provenanceOf(fileName: String): TemplateProvenance =
when {
fileName == TEMPLATE_CORE_ARCHIVE -> TemplateProvenance.BUNDLED
fileName.startsWith(PLUGIN_CGT_PREFIX) -> TemplateProvenance.PLUGIN
else -> TemplateProvenance.USER
}
if (fileName.startsWith(PLUGIN_CGT_PREFIX)) TemplateProvenance.PLUGIN else TemplateProvenance.USER

override suspend fun installTemplate(item: CgtFileItem): Result<Unit> =
withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -141,7 +146,9 @@ class TemplateRepositoryImpl(
withContext(Dispatchers.IO) {
try {
check(item.installed) { "'${item.name}' is not installed" }
check(item.provenance != TemplateProvenance.BUNDLED) { "Cannot uninstall the bundled template" }
Comment thread
jimturner-adfa marked this conversation as resolved.
// Guarded here as well as hidden by scanTemplates: once moved out, core.cgt is never
// listed again, so there would be no in-app way to put it back.
check(!isCoreArchive(item.name)) { "Cannot uninstall the bundled template" }

// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ data class TemplateMetadata(
* template ID: [com.itsaky.androidide.templates.Template.templateId] is a random UUID
* regenerated on every reload). Matches the convention used by
* `IdeTemplateServiceImpl`/`PluginProjectManager` when they write into `Environment.TEMPLATES_DIR`.
* The IDE's bundled `core.cgt` has no case here - it's excluded from the Templates tab entirely
* (see [com.itsaky.androidide.repositories.TemplateRepositoryImpl]).
*/
enum class TemplateProvenance {
/** The IDE's bundled `core.cgt`. */
BUNDLED,

/** Registered by a plugin (`plugin_<pluginId>_*.cgt`). */
PLUGIN,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,13 @@ fun TemplateListItem(
}
DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) {
if (item.installed) {
if (item.provenance != TemplateProvenance.BUNDLED) {
DropdownMenuItem(
text = { Text(stringResource(R.string.action_uninstall_template)) },
onClick = {
menuExpanded = false
onUninstall()
},
)
}
DropdownMenuItem(
text = { Text(stringResource(R.string.action_uninstall_template)) },
onClick = {
menuExpanded = false
onUninstall()
},
)
} else {
DropdownMenuItem(
text = { Text(stringResource(R.string.action_install_template)) },
Expand Down Expand Up @@ -178,7 +176,6 @@ fun TemplateListItem(

private fun TemplateProvenance.labelRes(): Int =
when (this) {
TemplateProvenance.BUNDLED -> R.string.template_provenance_bundled
TemplateProvenance.PLUGIN -> R.string.template_provenance_plugin
TemplateProvenance.USER -> R.string.template_provenance_user
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.itsaky.androidide.templates.manager.models.CgtFileItem
import com.itsaky.androidide.templates.manager.models.TemplateMetadata
import com.itsaky.androidide.templates.manager.models.TemplateProvenance
import kotlinx.coroutines.test.runTest
import org.adfa.constants.TEMPLATE_CORE_ARCHIVE
import org.junit.After
import org.junit.Before
import org.junit.Rule
Expand Down Expand Up @@ -214,4 +215,53 @@ class TemplateRepositoryImplTest {
assertThat(items.map { it.name }).containsExactly("installed.cgt", "other.cgt")
assertThat(items.filter { it.installed }.map { it.name }).containsExactly("installed.cgt")
}

@Test
fun listTemplateFiles_excludesTheBundledCoreArchive() =
runTest {
writeCgt(templatesDir, TEMPLATE_CORE_ARCHIVE)
writeCgt(templatesDir, "installed.cgt")

val items = repository.listTemplateFiles().getOrThrow()

assertThat(items.map { it.name }).containsExactly("installed.cgt")
}

@Test
fun listTemplateFiles_excludesTheCoreArchiveCaseInsensitively() =
runTest {
writeCgt(templatesDir, "Core.CGT")
writeCgt(templatesDir, "installed.cgt")

val items = repository.listTemplateFiles().getOrThrow()

assertThat(items.map { it.name }).containsExactly("installed.cgt")
}

@Test
fun uninstallTemplate_refusesTheCoreArchive_evenWhenNotReachedThroughTheList() =
runTest {
// A differently-cased name, built directly rather than via listTemplateFiles: the guard
// must hold on the operation itself, not rely on the list having hidden the item.
val core = File(templatesDir, "Core.cgt").apply { writeText("bundled") }

val result = repository.uninstallTemplate(item(core, installed = true))

assertThat(result.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java)
assertThat(core.readText()).isEqualTo("bundled")
assertThat(downloadDir.list()).isEmpty()
}

@Test
fun listTemplateFiles_excludesACoreArchiveFoundOnlyInDownloads() =
runTest {
// No core.cgt in templatesDir yet (e.g. asset bootstrap hasn't run) - the twin-hiding
// dedup above can't catch this case, since there's no installed name to match against.
// core.cgt must still be excluded outright, not shown as an installable USER template.
writeCgt(downloadDir, TEMPLATE_CORE_ARCHIVE)

val items = repository.listTemplateFiles().getOrThrow()

assertThat(items).isEmpty()
}
}
1 change: 0 additions & 1 deletion resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,6 @@
<string name="no_templates_found_hint">Templates you download show up here for installing</string>
<string name="status_template_installed">Installed</string>
<string name="status_template_not_installed">Not installed</string>
<string name="template_provenance_bundled">Bundled</string>
<string name="template_provenance_plugin">From plugin</string>
<string name="template_provenance_user">Imported</string>
<plurals name="template_contains_count">
Expand Down
Loading