From 13865e372821c7333bb2cfdde5eddb69873ef1a3 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Wed, 1 Apr 2026 14:13:40 -0700 Subject: [PATCH 1/3] template errors and warnings during project generation --- .../androidide/activities/MainActivity.kt | 5 +- .../editor/ProjectHandlerActivity.kt | 5 + .../fragments/TemplateDetailsFragment.kt | 3 +- .../fragments/TemplateListFragment.kt | 16 +- resources/src/main/res/values/strings.xml | 2 + .../itsaky/androidide/templates/template.kt | 4 +- .../templates/impl/TemplateProviderImpl.kt | 5 +- .../androidide/templates/impl/base/results.kt | 7 +- .../templates/impl/zip/ZipRecipeExecutor.kt | 181 ++++++++++++------ .../templates/impl/zip/ZipTemplateReader.kt | 3 + 10 files changed, 156 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt index 54508822df..09098b8140 100755 --- a/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt @@ -400,7 +400,7 @@ class MainActivity : EdgeToEdgeIDEActivity() { builder.show() } - internal fun openProject(root: File, project: RecentProject? = null) { + internal fun openProject(root: File, project: RecentProject? = null, hasTemplateIssues: Boolean = false) { ProjectManagerImpl.getInstance().projectPath = root.absolutePath GeneralPreferences.lastOpenedProject = root.absolutePath @@ -425,6 +425,9 @@ class MainActivity : EdgeToEdgeIDEActivity() { val intent = Intent(this, EditorActivityKt::class.java).apply { putExtra("PROJECT_PATH", root.absolutePath) + if (hasTemplateIssues) { + putExtra("HAS_TEMPLATE_ISSUES", true) + } addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) } diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt index 5853a547e9..04613e81ee 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt @@ -214,6 +214,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() { observeStates() startServices() + + if (intent.getBooleanExtra("HAS_TEMPLATE_ISSUES", false)) { + flashError(getString(string.msg_template_warnings)) + } + } private fun observeStates() { diff --git a/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt index 620076de75..cd4c2853ac 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt @@ -138,7 +138,8 @@ class TemplateDetailsFragment : // open the project (requireActivity() as MainActivity).openProject( result.data.projectDir, - project = project + project = project, + hasTemplateIssues = result.hasErrorsWarnings ) } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt index 15f867bf80..dd8f31feff 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt @@ -29,6 +29,8 @@ import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.idetooltips.TooltipTag.EXIT_TO_MAIN import com.itsaky.androidide.templates.ITemplateProvider import com.itsaky.androidide.templates.ProjectTemplate +import com.itsaky.androidide.templates.impl.TemplateProviderImpl +import com.itsaky.androidide.utils.flashError import com.itsaky.androidide.viewmodel.MainViewModel import org.slf4j.LoggerFactory @@ -115,11 +117,9 @@ class TemplateListFragment : log.debug("Reloading templates...") - val templates = - ITemplateProvider - .getInstance(reload = true) - .getTemplates() - .filterIsInstance() + val provider = ITemplateProvider.getInstance(reload = true) + val templates = provider.getTemplates().filterIsInstance() + val warnings = (provider as TemplateProviderImpl).warnings adapter = TemplateListAdapter( @@ -140,5 +140,9 @@ class TemplateListFragment : ) binding.list.adapter = adapter updateSpanCount() - } + + if (warnings.isNotEmpty()) { + requireActivity().flashError(warnings.joinToString("\n")) + } + } } diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index b7096d552d..fd1202270e 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1016,6 +1016,8 @@ Delete installation file after install %1$s: %2$s + \n\nProject creation finished with warnings/errors. Open IDE Logs for details. + Wireless debugging pairing diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt index 677fcc9a80..b80d8d26da 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt @@ -58,7 +58,9 @@ val data: D /** * Result of recipe execution for a [ProjectTemplate]. */ -interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData +interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData { + val hasErrorsWarnings: Boolean +} /** * Result of recipe execution for a [ModuleTemplate]. diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateProviderImpl.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateProviderImpl.kt index cb51989fdf..3bd8577450 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateProviderImpl.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateProviderImpl.kt @@ -44,6 +44,7 @@ class TemplateProviderImpl : ITemplateProvider { } private val templates = mutableMapOf>() + val warnings: MutableList = mutableListOf() init { reload() @@ -55,7 +56,7 @@ class TemplateProviderImpl : ITemplateProvider { for (zipFile in list) { try { - val zipTemplates = ZipTemplateReader.read(zipFile) { json, params, path, data, defModule -> + val zipTemplates = ZipTemplateReader.read(zipFile, warnings) { json, params, path, data, defModule -> ZipRecipeExecutor({ ZipFile(zipFile) }, json, params, path, data, defModule) } @@ -63,6 +64,7 @@ class TemplateProviderImpl : ITemplateProvider { templates[t.templateId] = t } } catch (e: Exception) { + warnings.add("Failed to load template archive $zipFile error: ${e.message}") log.error("Failed to load template from archive: $zipFile", e) } } @@ -78,6 +80,7 @@ class TemplateProviderImpl : ITemplateProvider { override fun reload() { release() + warnings.clear() initializeTemplates() } diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/results.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/results.kt index e4a7496419..976a52b759 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/results.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/results.kt @@ -25,7 +25,8 @@ import com.itsaky.androidide.templates.base.ModuleTemplateBuilder import com.itsaky.androidide.templates.base.ProjectTemplateBuilder data class ProjectTemplateRecipeResultImpl( - override val data: ProjectTemplateData + override val data: ProjectTemplateData, + override val hasErrorsWarnings: Boolean = false ) : ProjectTemplateRecipeResult data class ModuleTemplateRecipeResultImpl(override val data: ModuleTemplateData @@ -33,9 +34,9 @@ data class ModuleTemplateRecipeResultImpl(override val data: ModuleTemplateData internal fun ProjectTemplateBuilder.recipeResult(): ProjectTemplateRecipeResult { - return ProjectTemplateRecipeResultImpl(data) + return ProjectTemplateRecipeResultImpl(data) } internal fun ModuleTemplateBuilder.recipeResult(): ModuleTemplateRecipeResult { - return ModuleTemplateRecipeResultImpl(data) + return ModuleTemplateRecipeResultImpl(data) } \ No newline at end of file diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt index a7975e7f81..e5a662cc7e 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt @@ -18,6 +18,7 @@ import com.itsaky.androidide.templates.RecipeExecutor import com.itsaky.androidide.templates.TemplateRecipe import com.itsaky.androidide.templates.impl.base.ProjectTemplateRecipeResultImpl import com.itsaky.androidide.utils.Environment +import io.pebbletemplates.pebble.error.PebbleException import org.adfa.constants.ANDROID_GRADLE_PLUGIN_VERSION import org.adfa.constants.KOTLIN_VERSION @@ -32,6 +33,8 @@ class ZipRecipeExecutor( private val defModule: ModuleTemplateData, ) : TemplateRecipe { + var hasErrorsWarnings: Boolean = false + companion object { private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java) } @@ -40,11 +43,11 @@ class ZipRecipeExecutor( executor: RecipeExecutor ): ProjectTemplateRecipeResult { - log.debug("executor called!!") + info("Starting project creation for $basePath") val projectDir = data.projectDir if (projectDir.exists()) { - return ProjectTemplateRecipeResultImpl(data) + return ProjectTemplateRecipeResultImpl(data, hasErrorsWarnings) } val projectRoot = projectDir.canonicalFile @@ -56,69 +59,107 @@ class ZipRecipeExecutor( zipProvider().use { zip -> - val customSyntax = Syntax.Builder() - .setPrintOpenDelimiter(DELIM_PRINT_OPEN) - .setPrintCloseDelimiter(DELIM_PRINT_CLOSE) - .setExecuteOpenDelimiter(DELIM_EXECUTE_OPEN) - .setExecuteCloseDelimiter(DELIM_EXECUTE_CLOSE) - .setCommentOpenDelimiter(DELIM_COMMENT_OPEN) - .setCommentCloseDelimiter(DELIM_COMMENT_CLOSE) - .build() - - val pebbleEngine = PebbleEngine.Builder() - .loader(StringLoader()) - .syntax(customSyntax) - .build() - - val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params) - log.debug("identifiers warnings: ${warnings.joinToString(System.lineSeparator())}") - - val packageName = - resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) - - for (entry in zip.entries()) { - if (!entry.name.startsWith("$basePath/")) continue - if (entry.name == "$basePath/") continue - if (entry.name.startsWith("$basePath/$META_FOLDER/")) continue - - if ((metaJson.parameters?.optional?.language != null) && - (data.language != null) && - shouldSkipFile( - entry.name.removeSuffix(TEMPLATE_EXTENSION), - safeLanguageName(data.language) - ) - ) continue - - val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue - - val relativePath = normalized.removePrefix("$basePath/") - .replace(packageName.value, defModule.packageName.replace(".", "/")) - - val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile - - if (!outFile.toPath().startsWith(projectRoot.toPath())) { - log.warn("Skipping suspicious ZIP entry outside project dir: {}", entry.name) - continue + val customSyntax = Syntax.Builder() + .setPrintOpenDelimiter(DELIM_PRINT_OPEN) + .setPrintCloseDelimiter(DELIM_PRINT_CLOSE) + .setExecuteOpenDelimiter(DELIM_EXECUTE_OPEN) + .setExecuteCloseDelimiter(DELIM_EXECUTE_CLOSE) + .setCommentOpenDelimiter(DELIM_COMMENT_OPEN) + .setCommentCloseDelimiter(DELIM_COMMENT_CLOSE) + .build() + + val pebbleEngine = PebbleEngine.Builder() + .loader(StringLoader()) + .syntax(customSyntax) + .build() + + val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params) + if (warnings.isNotEmpty()) { + warn("Identifier warnings: ${warnings.joinToString(System.lineSeparator())}") } - if (entry.isDirectory) { - outFile.mkdirs() - } else { - outFile.parentFile?.mkdirs() - - if (entry.name.endsWith(TEMPLATE_EXTENSION)) { - log.debug("template processing ${entry.name}") - val content = zip.getInputStream(entry).bufferedReader().use { it.readText() } - val template = pebbleEngine.getTemplate(content) - val writer = StringWriter() - template.evaluate(writer, identifiers) - outFile.writeText(writer.toString(), Charsets.UTF_8) - } else { - zip.getInputStream(entry).use { input -> - outFile.outputStream().use { output -> - input.copyTo(output) + val packageName = + resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) + + for (entry in zip.entries()) { + if (!entry.name.startsWith("$basePath/")) continue + if (entry.name == "$basePath/") continue + if (entry.name.startsWith("$basePath/$META_FOLDER/")) continue + + if ((metaJson.parameters?.optional?.language != null) && + (data.language != null) && + shouldSkipFile( + entry.name.removeSuffix(TEMPLATE_EXTENSION), + safeLanguageName(data.language) + ) + ) continue + + val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue + + val relativePath = normalized.removePrefix("$basePath/") + .replace(packageName.value, defModule.packageName.replace(".", "/")) + + val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile + + if (!outFile.toPath().startsWith(projectRoot.toPath())) { + warn("Skipping suspicious template entry outside project dir: ${entry.name}") + continue + } + + if (entry.isDirectory) { + outFile.mkdirs() + } else { + try { + outFile.parentFile?.mkdirs() + + if (entry.name.endsWith(TEMPLATE_EXTENSION)) { + info("Processing template ${entry.name}") + val content = try { + zip.getInputStream(entry).bufferedReader().use { it.readText() } + } catch (e: Exception) { + throw e.wrap("Failed to read template ${entry.name}") + } + + val template = try { + pebbleEngine.getTemplate(content) + } catch (e: PebbleException) { + throw e.wrap( + "Pebble parse error in ${entry.name} at line ${e.lineNumber}: ${e.message}" + ) + } catch (e: Exception) { + throw e.wrap("Unexpected Pebble parse error in ${entry.name}") + } + + val writer = StringWriter() + try { + template.evaluate(writer, identifiers) + } catch (e: PebbleException) { + error( + "Pebble evaluation error in ${entry.name} at line ${e.lineNumber}: ${e.message}", e + ) + } catch (e: Exception) { + error("Unexpected Pebble evaluation error in ${entry.name}", e) + } + + try { + outFile.writeText(writer.toString(), Charsets.UTF_8) + } catch (e: Exception) { + error("Failed writing output file: ${outFile.absolutePath}", e) + } + + } else { + try { + zip.getInputStream(entry).use { input -> + outFile.outputStream().use { output -> + input.copyTo(output) + } + } + } catch (e: Exception) { + error("Failed copying binary entry: ${entry.name}", e) } } + } catch (e: Exception) { + error("Failed to process template entry: ${entry.name}", e) } } } @@ -126,7 +167,7 @@ class ZipRecipeExecutor( keystore(executor) - return ProjectTemplateRecipeResultImpl(data) + return ProjectTemplateRecipeResultImpl(data, hasErrorsWarnings) } private fun keystore(executor: RecipeExecutor) { @@ -266,4 +307,20 @@ class ZipRecipeExecutor( return normalizedParts.joinToString(File.separator) } + private fun warn(msg: String) { + hasErrorsWarnings = true + log.warn(msg) + } + + private fun info(msg: String) { + log.info(msg) + } + + private fun error(msg: String, e: Exception) { + hasErrorsWarnings = true + log.error(msg, e) + } + + private fun Exception.wrap(msg: String): RuntimeException = + RuntimeException(msg, this) } diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt index b619d7a5cb..7b5358fb12 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt @@ -26,6 +26,7 @@ object ZipTemplateReader { fun read( zipFile: File, + warnings: MutableList, recipeFactory: (TemplateJson, MutableMap>, String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe ): List { @@ -110,11 +111,13 @@ object ZipTemplateReader { templates.add(project) } catch (e: Exception) { + warnings.add("Failed to load template at ${templateRef.path} error: ${e.message}") log.error("Failed to load template at ${templateRef.path}", e) } } } } catch (e: Exception) { + warnings.add("Failed to load template archive $zipFile error: ${e.message}") log.error("Failed to read zip file $zipFile", e) } From 35a3b67fdbf97b9b2b35818174cdb763274d494f Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Wed, 1 Apr 2026 14:18:18 -0700 Subject: [PATCH 2/3] formatting --- .../main/java/com/itsaky/androidide/activities/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt index 09098b8140..8157467a1f 100755 --- a/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt @@ -416,7 +416,7 @@ class MainActivity : EdgeToEdgeIDEActivity() { } // Track project open in Firebase Analytics - analyticsManager.trackProjectOpened(root.absolutePath) + analyticsManager.trackProjectOpened(root.absolutePath) if (isFinishing) { return From af4458d513e3135bddf4ab005f0e3341cffbced6 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Wed, 1 Apr 2026 15:05:22 -0700 Subject: [PATCH 3/3] fixes --- .../com/itsaky/androidide/fragments/TemplateListFragment.kt | 2 +- .../main/java/com/itsaky/androidide/templates/template.kt | 1 + .../androidide/templates/impl/zip/ZipRecipeExecutor.kt | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt index dd8f31feff..56df340ab9 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt @@ -119,7 +119,7 @@ class TemplateListFragment : val provider = ITemplateProvider.getInstance(reload = true) val templates = provider.getTemplates().filterIsInstance() - val warnings = (provider as TemplateProviderImpl).warnings + val warnings = (provider as? TemplateProviderImpl)?.warnings.orEmpty() adapter = TemplateListAdapter( diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt index b80d8d26da..4a631e51f3 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/template.kt @@ -60,6 +60,7 @@ val data: D */ interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData { val hasErrorsWarnings: Boolean + get() = false } /** diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt index e5a662cc7e..efe91dbdd0 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt @@ -131,15 +131,18 @@ class ZipRecipeExecutor( } val writer = StringWriter() - try { + val rendered = try { template.evaluate(writer, identifiers) } catch (e: PebbleException) { error( "Pebble evaluation error in ${entry.name} at line ${e.lineNumber}: ${e.message}", e ) + null } catch (e: Exception) { error("Unexpected Pebble evaluation error in ${entry.name}", e) + null } + if (rendered == null) continue try { outFile.writeText(writer.toString(), Charsets.UTF_8)