From 52eccbe1ad2004d3a0a5d74b1d02a7c19984f1bd Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Thu, 12 Mar 2026 23:15:32 +0800 Subject: [PATCH 01/25] next gen template initial version --- .../adapters/TemplateListAdapter.kt | 24 +- .../fragments/TemplateDetailsFragment.kt | 9 +- .../fragments/TemplateListFragment.kt | 1 + .../itsaky/androidide/utils/Environment.java | 4 + .../main/java/org/adfa/constants/constants.kt | 4 + gradle/libs.versions.toml | 7 +- templates-api/build.gradle.kts | 2 + .../templates/base/ProjectTemplateBuilder.kt | 2 + .../itsaky/androidide/templates/base/base.kt | 101 ++++++++ .../itsaky/androidide/templates/template.kt | 27 ++- templates-impl/build.gradle.kts | 4 +- .../templates/impl/TemplateProviderImpl.kt | 72 +++--- .../androidide/templates/impl/zip/ZipJson.kt | 108 +++++++++ .../templates/impl/zip/ZipRecipeExecutor.kt | 224 ++++++++++++++++++ .../impl/zip/ZipTemplateConstants.kt | 36 +++ .../templates/impl/zip/ZipTemplateReader.kt | 105 ++++++++ 16 files changed, 681 insertions(+), 49 deletions(-) create mode 100644 templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt create mode 100644 templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt create mode 100644 templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt create mode 100644 templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt index 5f6616b31a..93baea093d 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt @@ -22,10 +22,12 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.blankj.utilcode.util.ConvertUtils +import com.bumptech.glide.Glide import com.google.android.material.shape.CornerFamily import com.itsaky.androidide.adapters.TemplateListAdapter.ViewHolder import com.itsaky.androidide.databinding.LayoutTemplateListItemBinding import com.itsaky.androidide.templates.Template +import org.slf4j.LoggerFactory /** * [RecyclerView.Adapter] for showing templates in a [RecyclerView]. @@ -37,6 +39,10 @@ class TemplateListAdapter( private val onClick: ((Template<*>, ViewHolder) -> Unit)? = null, private val onLongClick: ((Template<*>, View) -> Unit)? = null, ) : RecyclerView.Adapter() { + companion object { + private val log = LoggerFactory.getLogger(TemplateListAdapter::class.java) + } + private val templates = templates.toMutableList() class ViewHolder( @@ -62,13 +68,25 @@ class TemplateListAdapter( position: Int, ) { holder.binding.apply { - val template = templates[position] + val template = templates[position] if (template == Template.EMPTY) { root.visibility = View.INVISIBLE return@apply } - templateName.text = templateName.context.getString(template.templateName) - templateIcon.setImageResource(template.thumb) + + log.debug("template: $template") + templateName.text = template.templateNameStr + log.debug("text: ${template.templateNameStr} templateName.text: ${templateName.text}") + if (template.thumbData != null) { + log.debug("thumbData is not null") + Glide.with(templateIcon.context) + .asBitmap() + .load(template.thumbData) + .into(templateIcon) + } else { + templateIcon.setImageResource(template.thumb) + } + templateIcon.shapeAppearanceModel = templateIcon.shapeAppearanceModel .toBuilder() 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 16b6a02f8c..b83277ccd4 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt @@ -136,8 +136,10 @@ class TemplateDetailsFragment : name = result.data.name, createdAt = now, lastModified = now, - templateName = getString(template.templateName), - language = result.data.language.name +// templateName = getString(template.templateName), + templateName = template.templateNameStr, +// language = result.data.language.name + language = result.data.language?.name ?: "unknown" ) ) @@ -163,6 +165,7 @@ class TemplateDetailsFragment : template ?: return binding.widgets.adapter = TemplateWidgetsListAdapter(template.widgets) - binding.title.setText(template.templateName) + //binding.title.setText(template.templateName) + binding.title.text = template.templateNameStr } } \ No newline at end of file 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 0c03127156..71b6276753 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt @@ -127,6 +127,7 @@ class TemplateListFragment : .getTemplates() .filterIsInstance() + log.debug("templates: $templates") adapter = TemplateListAdapter( templates = templates, diff --git a/common/src/main/java/com/itsaky/androidide/utils/Environment.java b/common/src/main/java/com/itsaky/androidide/utils/Environment.java index a3e1e64637..6b80b83c23 100755 --- a/common/src/main/java/com/itsaky/androidide/utils/Environment.java +++ b/common/src/main/java/com/itsaky/androidide/utils/Environment.java @@ -120,6 +120,8 @@ public final class Environment { public static final String NDK_TAR_XZ = "ndk-cmake.tar.xz"; public static File NDK_DIR; + public static File TEMPLATES_DIR; + public static String getArchitecture() { return IDEBuildConfigProvider.getInstance().getCpuAbiName(); } @@ -188,6 +190,8 @@ public static void init(Context context) { NDK_DIR = new File(ANDROID_HOME, "ndk"); + TEMPLATES_DIR = mkdirIfNotExists(new File(ANDROIDIDE_HOME, "templates")); + isInitialized.set(true); } diff --git a/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt b/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt index 4899f6e7d8..90b65da21a 100644 --- a/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt +++ b/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt @@ -76,3 +76,7 @@ const val LOGSENDER_AAR_NAME = "logsender.aar" const val GRADLE_API_NAME_JAR = "gradle-api-$GRADLE_DISTRIBUTION_VERSION.jar" const val GRADLE_API_NAME_JAR_ZIP = "${GRADLE_API_NAME_JAR}.zip" const val GRADLE_API_NAME_JAR_BR = "${GRADLE_API_NAME_JAR}.br" + +// Templates archive +const val TEMPLATE_ARCHIVE_EXTENSION = "cgt" +const val TEMPLATE_CORE_ARCHIVE = "core$TEMPLATE_ARCHIVE_EXTENSION" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 433a98d96f..1a0d9bbae5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,6 +78,9 @@ compose-compiler = "2.1.21" leakcanary = "2.14" +pebble = "4.1.1" + + [libraries] # Dependencies in composite build @@ -220,7 +223,7 @@ androidx-vectors = { module = "androidx.vectordrawable:vectordrawable", version. androidx-animated_vectors = { module = "androidx.vectordrawable:vectordrawable-animated", version.ref = "androidx-vectordrawable" } androidx-core = { module = "androidx.core:core", version = "1.13.1" } androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.13.1" } -androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.6.2" } +#androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.6.2" } androidx-libDesugaring = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } androidx-transition = { module = "androidx.transition:transition-ktx", version = "1.5.1" } @@ -307,6 +310,8 @@ androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "m monitor = { group = "androidx.test", name = "monitor", version.ref = "monitorVersion" } org-json = { module = "org.json:json", version = "20210307"} +pebble = { module = "io.pebbletemplates:pebble", version.ref = "pebble" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/templates-api/build.gradle.kts b/templates-api/build.gradle.kts index dd97575406..fba5a80cda 100644 --- a/templates-api/build.gradle.kts +++ b/templates-api/build.gradle.kts @@ -39,4 +39,6 @@ dependencies { api(libs.androidx.annotation) api(libs.androidx.appcompat) api(libs.google.material) + + implementation(libs.google.gson) } diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt index e31494fa2c..ac3814ca9b 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt @@ -199,5 +199,7 @@ class ProjectTemplateBuilder : ExecutorDataTemplateBuilder baseFile( return FileTemplateBuilder(dir).apply(configurator) .build() as FileTemplate } + +/** + * Setup base files for zip project templates. + * + * @param block Function to configure the template. + */ +inline fun baseZipProject( + projectName: StringParameter = projectNameParameter(), + packageName: StringParameter = packageNameParameter(), + useKts: BooleanParameter = useKtsParameter(), + minSdk: EnumParameter = minSdkParameter(), + language: EnumParameter = projectLanguageParameter(), + projectVersionData: ProjectVersionData = ProjectVersionData(), + isToml: Boolean = false, + showUseKts: Boolean = false, + showMinSdk: Boolean = true, + showLanguage: Boolean = true, + crossinline block: ProjectTemplateBuilder.() -> Unit +): ProjectTemplate { + return ProjectTemplateBuilder().apply { + + // When project name is changed, change the package name accordingly + projectName.observe { name -> + val newPackage = + AndroidUtils.appNameToPackageName(name.value, packageName.value) + packageName.setValue(newPackage) + } + + Environment.mkdirIfNotExists(Environment.PROJECTS_DIR) + + val saveLocation = stringParameter { + name = R.string.wizard_save_location + default = Environment.PROJECTS_DIR.absolutePath + endIcon = { R.drawable.ic_folder } + constraints = listOf(NONEMPTY, DIRECTORY, EXISTS) + inputType = + android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE + maxLines = 1 + tooltipTag = "setup.save.location" + } + + projectName.doBeforeCreateView { + it.setValue(getNewProjectName(saveLocation.value, projectName.value)) + } + + widgets( + TextFieldWidget(projectName), TextFieldWidget(packageName), + TextFieldWidget(saveLocation) + ) + + if (showLanguage) { + widgets(SpinnerWidget(language)) + } + + if (showMinSdk) { + widgets(SpinnerWidget(minSdk)) + } + + if (showUseKts) { + widgets(CheckBoxWidget(useKts)) + } + + // Setup the required properties before executing the recipe + preRecipe = { + this@apply._executor = this + + if (!showUseKts) { + useKts.setValue(true, notify = false) + } + + this@apply._data = ProjectTemplateData( + projectName.value, + File(saveLocation.value, projectName.value), + projectVersionData, + language = if (showLanguage) language.value else null, + useKts = useKts.value, + useToml = isToml + ) + + if (data.projectDir.exists() && data.projectDir.listFiles() + ?.isNotEmpty() == true + ) { + throw IllegalArgumentException("Project directory already exists") + } + + setDefaultModuleData( + ModuleTemplateData( + ":app", appName = data.name, packageName.value, + data.moduleNameToDir(":app"), type = AndroidApp, + language = if (showLanguage) language.value else null, + minSdk = if (showMinSdk) minSdk.value else null, + useKts = data.useKts, useToml = isToml + ) + ) + } + + block() + + }.build() as ProjectTemplate +} 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 55e70297f0..677fcc9a80 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 @@ -106,7 +106,7 @@ typealias TemplateRecipeFinalizer = RecipeExecutor.() -> Unit * @property language The source language for the module. * @property useKts Whether to use Kotlin DSL for Gradle build scripts. */ -abstract class BaseTemplateData(val name: String, val projectDir: File, val language: Language, +abstract class BaseTemplateData(val name: String, val projectDir: File, var language: Language?, val useKts: Boolean, val useToml: Boolean = false) : TemplateData() { /** @@ -176,7 +176,7 @@ open class ProjectVersionData(val gradlePlugin: String = ANDROID_GRADLE_PLUGIN_V * @property targetSdk The target SDK version for modules. * @property buildTools The build tools version for modules. */ -data class ModuleVersionData(val minSdk: Sdk, +data class ModuleVersionData(val minSdk: Sdk?, val targetSdk: Sdk = TARGET_SDK_VERSION, val compileSdk: Sdk = COMPILE_SDK_VERSION, val javaSource: String = JAVA_SOURCE_VERSION, @@ -203,7 +203,7 @@ private fun javaVersionPrefix(version: String): String = "JavaVersion.VERSION_${ * @property version The version information for this project. */ class ProjectTemplateData(name: String, projectDir: File, val version: ProjectVersionData, -language: Language, useKts: Boolean, useToml: Boolean = false) : BaseTemplateData(name, projectDir, language, useKts, useToml) +language: Language?, useKts: Boolean, useToml: Boolean = false) : BaseTemplateData(name, projectDir, language, useKts, useToml) /** * Data for creating module projects. @@ -217,7 +217,7 @@ language: Language, useKts: Boolean, useToml: Boolean = false) : BaseTemplateDat * So in future we would not get confused. */ open class ModuleTemplateData(name: String, val appName: String?, val packageName: String, - projectDir: File, val type: ModuleType, language: Language, useKts: Boolean = true, minSdk: Sdk, + projectDir: File, val type: ModuleType, language: Language?, useKts: Boolean = true, minSdk: Sdk?, val versions: ModuleVersionData = ModuleVersionData(minSdk), useToml: Boolean = false) : BaseTemplateData(name, projectDir, language, useKts, useToml) { @@ -233,12 +233,14 @@ fun srcFolder(srcSet: SrcSet): File { /** * Model for a template. * - * @property templateName The name of the template. + * @property templateName The name id of the template. + * @property templateNameStr The name of the template. * @property thumb The thumbnail for the template. */ open class Template(@StringRes open val templateName: Int, -@DrawableRes open val thumb: Int, open val tooltipTag: String?, open val widgets: List>, -open val recipe: TemplateRecipe) { + @DrawableRes open val thumb: Int, open val tooltipTag: String?, open val widgets: List>, + open val recipe: TemplateRecipe, open val templateNameStr: String = "", open val thumbData: ByteArray? = null +) { /** * The ID for this template. @@ -263,8 +265,8 @@ companion object { open class ProjectTemplate(val moduleTemplates: List>, @StringRes templateName: Int, @DrawableRes thumb: Int, tooltipTag: String?, widgets: List>, -recipe: TemplateRecipe) : -Template(templateName, thumb, tooltipTag, widgets, recipe) { +recipe: TemplateRecipe, templateNameStr: String, thumbData: ByteArray?) : +Template(templateName, thumb, tooltipTag, widgets, recipe, templateNameStr, thumbData) { override val parameters: Collection> get() = if (moduleTemplates.isEmpty()) super.parameters else super.parameters.toMutableList() @@ -319,7 +321,9 @@ widgets: List>, recipe: TemplateRecipe) : Template(name, thumb, abstract class TemplateBuilder( @StringRes open var templateName: Int? = null, @DrawableRes open var thumb: Int? = null, open var tooltipTag: String? = null, -open var widgets: List>? = null, open var recipe: TemplateRecipe? = null) { +open var widgets: List>? = null, open var recipe: TemplateRecipe? = null, +open var templateNameStr: String? = null, open var thumbData: ByteArray? = null +) { /** * Adds the given widgets to the widgets list. @@ -338,9 +342,10 @@ fun widgets(vararg widgets: Widget<*>) { } fun build(): Template { - requireNotNull(templateName) { "Template must have a name" } + requireNotNull(templateName) { "Template must have a name id" } requireNotNull(thumb) { "Template must have a thumbnail" } requireNotNull(recipe) { "Template must have a recipe" } + requireNotNull(templateNameStr) {"Template must have a name"} this.widgets = this.widgets ?: emptyList() diff --git a/templates-impl/build.gradle.kts b/templates-impl/build.gradle.kts index 19c7e7bba5..e9aa88ea52 100644 --- a/templates-impl/build.gradle.kts +++ b/templates-impl/build.gradle.kts @@ -41,7 +41,9 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.google.auto.service.annotations) - testImplementation(projects.templatesApi) + implementation(libs.pebble) + + testImplementation(projects.templatesApi) testImplementation(projects.lsp.api) testImplementation(projects.preferences) testImplementation(projects.testing.unit) 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 70312efa4f..c5a24d031d 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 @@ -21,16 +21,15 @@ import com.google.auto.service.AutoService import com.google.common.collect.ImmutableList import com.itsaky.androidide.templates.ITemplateProvider import com.itsaky.androidide.templates.Template -import com.itsaky.androidide.templates.impl.basicActivity.basicActivityProject -import com.itsaky.androidide.templates.impl.bottomNavActivity.bottomNavActivityProject -import com.itsaky.androidide.templates.impl.composeActivity.composeActivityProject -import com.itsaky.androidide.templates.impl.emptyActivity.emptyActivityProject -import com.itsaky.androidide.templates.impl.navDrawerActivity.navDrawerActivityProject -import com.itsaky.androidide.templates.impl.noActivity.noActivityProjectTemplate -import com.itsaky.androidide.templates.impl.noAndroidXActivity.noAndroidXActivityProject -import com.itsaky.androidide.templates.impl.pluginProject.pluginProjectTemplate -import com.itsaky.androidide.templates.impl.tabbedActivity.tabbedActivityProject -import com.itsaky.androidide.templates.impl.ndkActivity.ndkActivityProject +import com.itsaky.androidide.templates.impl.zip.ZipRecipeExecutor +import com.itsaky.androidide.templates.impl.zip.ZipTemplateReader + +import org.adfa.constants.TEMPLATE_ARCHIVE_EXTENSION +import com.itsaky.androidide.utils.Environment.TEMPLATES_DIR + +import org.slf4j.LoggerFactory +import java.io.File +import java.util.zip.ZipFile /** * Default implementation of the [ITemplateProvider]. @@ -41,33 +40,44 @@ import com.itsaky.androidide.templates.impl.ndkActivity.ndkActivityProject @AutoService(ITemplateProvider::class) class TemplateProviderImpl : ITemplateProvider { + companion object { + private val log = LoggerFactory.getLogger(TemplateProviderImpl::class.java) + } + private val templates = mutableMapOf>() init { - initializeTemplates() + reload() } - private fun templates() = - //@formatter:off - listOfNotNull( - noActivityProjectTemplate(), - emptyActivityProject(), - basicActivityProject(), - navDrawerActivityProject(), - bottomNavActivityProject(), - tabbedActivityProject(), - noAndroidXActivityProject(), - composeActivityProject(), - pluginProjectTemplate(), - ndkActivityProject() - ) - private fun initializeTemplates() { - templates().forEach { template -> - templates[template.templateId] = template + // val folder = File("/sdcard/Download/templates") + val folder = TEMPLATES_DIR + log.debug("Template listing archives in: ${folder.toString()} with extension: ${TEMPLATE_ARCHIVE_EXTENSION}") + val list = folder.listFiles { file -> file.extension == TEMPLATE_ARCHIVE_EXTENSION } ?: return + + for (zipFile in list) { + log.debug("Template archive: $zipFile") + try { + val zipTemplates = ZipTemplateReader.read(zipFile) { json, path, /* params */ data, defModule -> + ZipRecipeExecutor({ ZipFile(zipFile) }, json, path, /* params */ data, defModule) + } + + if (zipTemplates != null) { + for (t in zipTemplates) { + log.debug("template: $t") + if (t != null) { + templates[t.templateId] = t + } + } + } + + log.debug("templates: $templates") + } catch (e: Throwable) { + e.printStackTrace() + } } } - //@formatter:on override fun getTemplates(): List> { return ImmutableList.copyOf(templates.values) @@ -79,7 +89,9 @@ class TemplateProviderImpl : ITemplateProvider { override fun reload() { release() - initializeTemplates() + // CoroutineScope(Dispatchers.IO).launch { + initializeTemplates() + // } } override fun release() { diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt new file mode 100644 index 0000000000..1a644cb138 --- /dev/null +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt @@ -0,0 +1,108 @@ +package com.itsaky.androidide.templates.impl.zip + +import com.itsaky.androidide.templates.BooleanParameterBuilder +import com.itsaky.androidide.templates.CheckBoxWidget +import com.itsaky.androidide.templates.StringParameterBuilder +import com.itsaky.androidide.templates.TextFieldWidget +import com.itsaky.androidide.templates.Widget + +data class TemplatesIndex(val templates: List) +data class TemplateRef(val path: String, val experimental: Boolean = false,) + +// data class TemplateJson( +// val name: String, +// val description: String?, +// val widgets: List = emptyList(), +// val language: Boolean? = true +// ) + +data class TemplateJson( + val name: String, + val description: String?, + val version: String?, + val parameters: ParametersJson? = null, + val system: SystemParametersJson? = null +) + +data class ParametersJson( + val required: RequiredParametersJson? = null, + val optional: OptionalParametersJson? = null, + val user: UserParametersJson? = null +) + +data class RequiredParametersJson( + val appName: IdentifierJson, + val packageName: IdentifierJson, + val saveLocation: IdentifierJson +) + +data class OptionalParametersJson( + val language: IdentifierJson? = null, + val minsdk: IdentifierJson? = null +) + +data class SystemParametersJson( + val agpVersion: IdentifierJson, + val kotlinVersion: IdentifierJson, + val gradleVersion: IdentifierJson, + val compileSdk: IdentifierJson, + val targetSdk: IdentifierJson, + val javaSourceCompat: IdentifierJson, + val javaTargetCompat: IdentifierJson, + val javaTarget: IdentifierJson +) + +data class IdentifierJson( + val identifier: String +) + +data class UserParametersJson( + val text: List = emptyList(), + val checkbox: List = emptyList() +) + +data class TextParameterJson( + val label: String, + val identifier: String, + val default: String? = null +) + +data class CheckboxParameterJson( + val label: String, + val identifier: String, + val default: Boolean? = null +) + +data class WidgetJson( + val type: String, + val name: String, + val id: String, + val default: Any?, + val options: List?, + val constraints: List? +) { + fun toWidget(): Widget<*>? { + return when(type) { + "string" -> { + val param = StringParameterBuilder().apply { + //this.name = this@WidgetJson.name + //this.description = null + //this.default = this@WidgetJson.default?.toString() ?: "" + //this.key = this@WidgetJson.id + }.build() + TextFieldWidget(param) + } + "boolean" -> { + val param = BooleanParameterBuilder().apply { + //this.name = this@WidgetJson.name + this.description = null + this.default = this@WidgetJson.default?.toString()?.toBoolean() ?: false + //this.key = this@WidgetJson.id + }.build() + CheckBoxWidget(param) + } + // Add more mappings as needed + else -> null + } + } +} 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 new file mode 100644 index 0000000000..1626888f67 --- /dev/null +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt @@ -0,0 +1,224 @@ +package com.itsaky.androidide.templates.impl.zip + +import com.itsaky.androidide.templates.Language +import java.io.File +import java.io.FileOutputStream +import java.io.StringWriter +import java.util.zip.ZipFile + +import org.slf4j.LoggerFactory +import io.pebbletemplates.pebble.PebbleEngine +import io.pebbletemplates.pebble.loader.StringLoader +import io.pebbletemplates.pebble.lexer.Syntax + +import com.itsaky.androidide.templates.ModuleTemplateData +import com.itsaky.androidide.templates.ProjectTemplateData +import com.itsaky.androidide.templates.ProjectTemplateRecipeResult +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 org.adfa.constants.ANDROID_GRADLE_PLUGIN_VERSION +import org.adfa.constants.KOTLIN_VERSION +import org.adfa.constants.Sdk + +class ZipRecipeExecutor( + private val zipProvider: () -> ZipFile, + private val metaJson: TemplateJson, + private val basePath: String, + private val data: ProjectTemplateData, + private val defModule: ModuleTemplateData, +) : TemplateRecipe { + + companion object { + + private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java) + } + + override fun execute( + executor: RecipeExecutor + ): ProjectTemplateRecipeResult { + + log.debug("executor called!!") + val zip = zipProvider() + val projectDir = data.projectDir + if (projectDir.exists()) { + return ProjectTemplateRecipeResultImpl(data) + } + + 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 (params, warnings) = metaJson.pebbleParams(data, defModule) + log.debug("params warnings: $warnings") + + log.debug("defModule: $defModule") + + 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.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 relativePath = entry.name.removePrefix("$basePath/") + .replace(packageName.value, defModule.packageName.replace(".", "/")) + + val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)) + + if (entry.isDirectory) { + outFile.mkdirs() + } else { + outFile.parentFile?.mkdirs() + val content = zip.getInputStream(entry).bufferedReader().readText() + + zip.getInputStream(entry).use { input -> + FileOutputStream(outFile).use { output -> + if (entry.name.endsWith(TEMPLATE_EXTENSION)) { + log.debug("template processing ${entry.name}") + val template = pebbleEngine.getTemplate(content) + val writer = StringWriter() + template.evaluate(writer, params) + outFile.writeText(writer.toString(), Charsets.UTF_8) + } else { + input.copyTo(output) + } + } + } + } + } + + keystore(executor) + + return ProjectTemplateRecipeResultImpl(data) + } + + private fun keystore(executor: RecipeExecutor) { + val storeSrc = Environment.KEYSTORE_RELEASE + val storeDest = File(data.projectDir, Environment.KEYSTORE_RELEASE_NAME) + if (storeSrc.exists()) { + executor.copy(storeSrc, storeDest) + } + + + val propsSrc = Environment.KEYSTORE_PROPERTIES + val propsDest = File(data.projectDir, Environment.KEYSTORE_PROPERTIES_NAME) + if (propsSrc.exists()) { + executor.copy(propsSrc, propsDest) + } + } + + private fun shouldSkipFile(name: String, language: String): Boolean { + // If language is Kotlin, skip .java files + // If language is Java, skip .kt files + val ext = name.substringAfterLast('.', "").lowercase() + return when (language.lowercase()) { + LANGUAGE_KOTLIN -> ext == FILE_EXT_JAVA + LANGUAGE_JAVA -> ext == FILE_EXT_KOTLIN + else -> false + } + } + + fun safeLanguageName(language: Language?): String = + language?.name?.lowercase() ?: "" + + fun safeMinSdkApi(minSdk: Sdk?): String = + minSdk?.api?.toString() ?: "" + + fun TemplateJson.pebbleParams( + data: ProjectTemplateData, + defModule: ModuleTemplateData + ): Pair, List> { + + val warnings = mutableListOf() + + val appName = resolveString(parameters?.required?.appName?.identifier, KEY_APP_NAME) + if (appName.usedDefault) warnings += "Missing 'appName', defaulted to $KEY_APP_NAME" + + val packageName = resolveString(parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) + if (packageName.usedDefault) warnings += "Missing 'packageName', defaulted to $KEY_PACKAGE_NAME" + + val saveLocation = resolveString(parameters?.required?.saveLocation?.identifier, KEY_SAVE_LOCATION) + if (saveLocation.usedDefault) warnings += "Missing 'saveLocation', defaulted to $KEY_SAVE_LOCATION" + + val language = resolveString(parameters?.optional?.language?.identifier, KEY_LANGUAGE) + if (language.usedDefault) warnings += "Missing 'language', defaulted to $KEY_LANGUAGE" + + val minSdk = resolveString(parameters?.optional?.minsdk?.identifier, KEY_MIN_SDK) + if (minSdk.usedDefault) warnings += "Missing 'minsdk', defaulted to $KEY_MIN_SDK" + + val agpVersion = resolveString(system?.agpVersion?.identifier, KEY_AGP_VERSION) + if (agpVersion.usedDefault) warnings += "Missing 'agpVersion', defaulted to $KEY_AGP_VERSION" + + val kotlinVersion = resolveString(system?.kotlinVersion?.identifier, KEY_KOTLIN_VERSION) + if (kotlinVersion.usedDefault) warnings += "Missing 'kotlinVersion', defaulted to $KEY_KOTLIN_VERSION" + + val gradleVersion = resolveString(system?.gradleVersion?.identifier, KEY_GRADLE_VERSION) + if (gradleVersion.usedDefault) warnings += "Missing 'gradleVersion', defaulted to $KEY_GRADLE_VERSION" + + val compileSdk = resolveString(system?.compileSdk?.identifier, KEY_COMPILE_SDK) + if (compileSdk.usedDefault) warnings += "Missing 'compileSdk', defaulted to $KEY_COMPILE_SDK" + + val targetSdk = resolveString(system?.targetSdk?.identifier, KEY_TARGET_SDK) + if (targetSdk.usedDefault) warnings += "Missing 'targetSdk', defaulted to $KEY_TARGET_SDK" + + val javaSourceCompat = resolveString(system?.javaSourceCompat?.identifier, KEY_JAVA_SOURCE_COMPAT) + if (javaSourceCompat.usedDefault) warnings += "Missing 'javaSourceCompat', defaulted to $KEY_JAVA_SOURCE_COMPAT" + + val javaTargetCompat = resolveString(system?.javaTargetCompat?.identifier, KEY_JAVA_TARGET_COMPAT) + if (javaTargetCompat.usedDefault) warnings += "Missing 'javaTargetCompat', defaulted to $KEY_JAVA_TARGET_COMPAT" + + val javaTarget = resolveString(system?.javaTarget?.identifier, KEY_JAVA_TARGET) + if (javaTarget.usedDefault) warnings += "Missing 'javaTarget', defaulted to $KEY_JAVA_TARGET" + + val map = mapOf( + appName.value to data.name, + packageName.value to defModule.packageName, + saveLocation.value to data.projectDir.toString(), + language.value to safeLanguageName(data.language), + minSdk.value to safeMinSdkApi(defModule.versions.minSdk), + agpVersion.value to ANDROID_GRADLE_PLUGIN_VERSION, + kotlinVersion.value to KOTLIN_VERSION, + gradleVersion.value to data.version.gradle, + compileSdk.value to defModule.versions.compileSdk.api.toString(), + targetSdk.value to defModule.versions.targetSdk.api.toString(), + javaSourceCompat.value to defModule.versions.javaSource(), + javaTargetCompat.value to defModule.versions.javaTarget(), + javaTarget.value to defModule.versions.javaTarget + ) + + return map to warnings + } + + data class ResolvedParam( + val value: T, + val usedDefault: Boolean + ) + + fun resolveString(value: String?, default: String): ResolvedParam { + return if (value.isNullOrBlank()) ResolvedParam(default, true) + else ResolvedParam(value, false) + } + + fun resolveBoolean(raw: Boolean?, default: Boolean): ResolvedParam { + return if (raw == null) ResolvedParam(default, true) + else ResolvedParam(raw, false) + } + +} diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt new file mode 100644 index 0000000000..0afb7c77d8 --- /dev/null +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateConstants.kt @@ -0,0 +1,36 @@ +package com.itsaky.androidide.templates.impl.zip + +const val ARCHIVE_JSON = "templates.json" + +const val META_FOLDER = "template" +const val META_JSON = "template.json" +const val META_THUMBNAIL = "thumb.png" + +const val TEMPLATE_EXTENSION = ".peb" + +const val DELIM_PRINT_OPEN = "\${{" +const val DELIM_PRINT_CLOSE = "}}" +const val DELIM_EXECUTE_OPEN = "\${%" +const val DELIM_EXECUTE_CLOSE = "%}" +const val DELIM_COMMENT_OPEN = "\${#" +const val DELIM_COMMENT_CLOSE = "#}" + +const val KEY_PACKAGE_NAME = "PACKAGE_NAME" +const val KEY_APP_NAME = "APP_NAME" +const val KEY_SAVE_LOCATION = "SAVE_LOCATION" +const val KEY_AGP_VERSION = "AGP_VERSION" +const val KEY_KOTLIN_VERSION = "KOTLIN_VERSION" +const val KEY_GRADLE_VERSION = "GRADLE_VERSION" +const val KEY_LANGUAGE = "LANGUAGE" +const val KEY_COMPILE_SDK = "COMPILE_SDK" +const val KEY_MIN_SDK = "MIN_SDK" +const val KEY_TARGET_SDK = "TARGET_SDK" +const val KEY_JAVA_SOURCE_COMPAT = "JAVA_SOURCE_COMPAT" +const val KEY_JAVA_TARGET_COMPAT = "JAVA_TARGET_COMPAT" +const val KEY_JAVA_TARGET = "JAVA_TARGET" + +const val LANGUAGE_KOTLIN = "kotlin" +const val LANGUAGE_JAVA = "java" + +const val FILE_EXT_KOTLIN = "kt" +const val FILE_EXT_JAVA = "java" 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 new file mode 100644 index 0000000000..5a8cb7b2de --- /dev/null +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt @@ -0,0 +1,105 @@ +package com.itsaky.androidide.templates.impl.zip + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.itsaky.androidide.templates.ModuleTemplateData +import com.itsaky.androidide.templates.ProjectTemplate +import com.itsaky.androidide.templates.ProjectTemplateData +import com.itsaky.androidide.templates.ProjectTemplateRecipeResult +import com.itsaky.androidide.templates.R +import com.itsaky.androidide.templates.TemplateRecipe +import com.itsaky.androidide.templates.Widget +import com.itsaky.androidide.templates.base.baseZipProject +import org.slf4j.LoggerFactory +import java.io.File +import java.util.zip.ZipFile + +object ZipTemplateReader { + private val log = LoggerFactory.getLogger(ZipTemplateReader::class.java) + + private val gson = Gson() + + fun read( + zipFile: File, + recipeFactory: (TemplateJson, /* List>?, */ String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe + ): List { + + val templates = mutableListOf() + + try { + val zip = ZipFile(zipFile) + + log.debug("zipFile: $zipFile zip: $zip") + + val indexEntry = zip.getEntry(ARCHIVE_JSON) ?: return emptyList() + val indexJson = zip.getInputStream(indexEntry).bufferedReader().use { + gson.fromJson(it, TemplatesIndex::class.java) + } + + log.debug("indexJson: $indexJson") + for (templateRef in indexJson.templates) { + try { + val basePath = templateRef.path + log.debug("basePath: $basePath") + val metaEntry = zip.getEntry("$basePath/$META_FOLDER/$META_JSON") ?: continue + + val metaJsonString = zip.getInputStream(metaEntry).bufferedReader().use { reader -> + reader.readText() + } + + val metaJson = gson.fromJson(metaJsonString, TemplateJson::class.java) + + log.debug("metaJson: $metaJson") + + val thumbEntry = zip.getEntry("$basePath/$META_FOLDER/$META_THUMBNAIL") + val thumbData = thumbEntry?.let { zip.getInputStream(it).use { s -> s.readBytes() } } + + if (thumbData == null) log.error("template $basePath/$META_FOLDER/$META_THUMBNAIL not found or is invalid") + log.debug("thumbData: $thumbData") + + // val widgets: List> = metaJson.widgets.mapNotNull { it.toWidget() } + // val widgets: List> = widgetsEntry?.let { + // val widgetsText = zip.getInputStream(it).bufferedReader().use { r -> r.readText() } + // parseWidgetsFromJson(widgetsText) + // } ?: metaJson.widgets.mapNotNull { it.toWidget() } + + val project = baseZipProject ( + showLanguage = (metaJson.parameters?.optional?.language != null), + showMinSdk = (metaJson.parameters?.optional?.minsdk != null) + ) { + + this.templateNameStr = metaJson.name + this.thumbData = thumbData + + this.templateName = 0 + this.thumb = R.drawable.template_no_activity + + log.debug("this.name: ${this.templateNameStr}") + this.recipe = TemplateRecipe { executor -> + val innerRecipe = recipeFactory(metaJson, basePath, /* params*/ data, defModule) + innerRecipe.execute(executor) + } + } + + log.debug("adding project ${metaJson.name}") + templates.add(project) + } catch (e: Exception) { + log.error("Failed to load template at ${templateRef.path}", e) + } + } + } catch (e: Exception) { + log.error("Failed to read zip file $zipFile", e) + // return emptyList() + } + + return templates + } + + + private fun parseWidgetsFromJson(jsonText: String): List> { + val type = object : TypeToken>() {}.type + val widgetJsons: List = gson.fromJson(jsonText, type) + return widgetJsons.mapNotNull { it.toWidget() } + } + +} From 5a5f8e19d7b39b63cf3240637253249aedc1eec8 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 13 Mar 2026 00:09:38 +0800 Subject: [PATCH 02/25] indent issue --- .../java/com/itsaky/androidide/fragments/TemplateListFragment.kt | 1 - 1 file changed, 1 deletion(-) 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 71b6276753..0c03127156 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt @@ -127,7 +127,6 @@ class TemplateListFragment : .getTemplates() .filterIsInstance() - log.debug("templates: $templates") adapter = TemplateListAdapter( templates = templates, From 02f97baa3afbffa772dd6cddfa91367c78f0143a Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 13 Mar 2026 11:57:23 +0800 Subject: [PATCH 03/25] update minsdk reference --- .../templates/base/modules/android/buildGradle.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/modules/android/buildGradle.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/modules/android/buildGradle.kt index 72be971037..0fa07f55ef 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/modules/android/buildGradle.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/modules/android/buildGradle.kt @@ -97,7 +97,7 @@ android { defaultConfig { applicationId = "${data.packageName}" - minSdk = ${data.versions.minSdk.api} + minSdk = ${data.versions.minSdk?.api} targetSdk = ${if (isComposeModule) data.versions.composeSdk.api else data.versions.targetSdk.api} versionCode = 1 versionName = "1.0" @@ -188,7 +188,7 @@ android { defaultConfig { applicationId = "${data.packageName}" - minSdk = ${data.versions.minSdk.api} + minSdk = ${data.versions.minSdk?.api} targetSdk = ${data.versions.targetSdk.api} versionCode = 1 versionName = "1.0" @@ -448,7 +448,7 @@ android { defaultConfig { applicationId = "${data.packageName}" - minSdk = ${data.versions.minSdk.api} + minSdk = ${data.versions.minSdk?.api} targetSdk = ${if (isComposeModule) data.versions.composeSdk.api else data.versions.targetSdk.api} versionCode = 1 versionName = "1.0" @@ -558,7 +558,7 @@ android { defaultConfig { applicationId = "${data.packageName}" - minSdk = ${data.versions.minSdk.api} + minSdk = ${data.versions.minSdk?.api} targetSdk = ${data.versions.targetSdk.api} versionCode = 1 versionName = "1.0" From 14731f27865e2491b2ea56393cdeee820425edb4 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 13 Mar 2026 20:39:27 +0800 Subject: [PATCH 04/25] template archive installation --- app/build.gradle.kts | 15 ++++++++++++++- .../androidide/assets/AssetsInstallationHelper.kt | 4 +++- .../androidide/assets/BundledAssetsInstaller.kt | 14 +++++++++++++- .../androidide/assets/SplitAssetsInstaller.kt | 12 +++++++++++- .../src/main/java/org/adfa/constants/constants.kt | 3 ++- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 34538dfb6b..44f14d5bc1 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,7 +198,7 @@ configurations.matching { it.name.contains("AndroidTest") }.configureEach { } dependencies { - debugImplementation(libs.common.leakcanary) + //debugImplementation(libs.common.leakcanary) // Annotation processors kapt(libs.common.glide.ap) @@ -541,6 +541,7 @@ fun createAssetsZip(arch: String) { "documentation.db", bootstrapName, "plugin-artifacts.zip", + "core.cgt" ).forEach { fileName -> val filePath = sourceDir.resolve(fileName) if (!filePath.exists()) { @@ -1059,6 +1060,12 @@ val debugAssets = "localMvnRepository.zip", "debug", ), + Asset( + "assets/core.cgt", + "https://appdevforall.org/dev-assets/debug/core.cgt", + "core.cgt", + "debug", + ), ) val releaseAssets = @@ -1111,6 +1118,12 @@ val releaseAssets = "v8/bootstrap.zip.br", "release", ), + Asset( + "assets/release/common/data/common/core.cgt.br", + "https://appdevforall.org/dev-assets/release/core.cgt.br", + "core.cgt.br", + "release", + ), ) fun assetsBatch( diff --git a/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt b/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt index b339c49bbc..7901d7f50c 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt @@ -20,6 +20,7 @@ import org.adfa.constants.DOCUMENTATION_DB import org.adfa.constants.GRADLE_API_NAME_JAR_ZIP import org.adfa.constants.GRADLE_DISTRIBUTION_ARCHIVE_NAME import org.adfa.constants.LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME +import org.adfa.constants.TEMPLATE_CORE_ARCHIVE import org.slf4j.LoggerFactory import com.itsaky.androidide.resources.R import java.io.File @@ -113,7 +114,8 @@ object AssetsInstallationHelper { BOOTSTRAP_ENTRY_NAME, GRADLE_API_NAME_JAR_ZIP, LLAMA_AAR, - PLUGIN_ARTIFACTS_ZIP + PLUGIN_ARTIFACTS_ZIP, + TEMPLATE_CORE_ARCHIVE, ) val stagingDir = Files.createTempDirectory(UUID.randomUUID().toString()) diff --git a/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt b/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt index d96eba2114..202a9ebf48 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt @@ -20,6 +20,8 @@ import org.adfa.constants.GRADLE_API_NAME_JAR_BR import org.adfa.constants.GRADLE_API_NAME_JAR_ZIP import org.adfa.constants.GRADLE_DISTRIBUTION_ARCHIVE_NAME import org.adfa.constants.LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME +import org.adfa.constants.TEMPLATE_CORE_ARCHIVE +import org.adfa.constants.TEMPLATE_CORE_ARCHIVE_BR import org.slf4j.LoggerFactory import java.io.File import java.io.FileNotFoundException @@ -78,7 +80,17 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() { } } - AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { + TEMPLATE_CORE_ARCHIVE -> { + val assetPath = ToolsManager.getCommonAsset(TEMPLATE_CORE_ARCHIVE_BR) + BrotliInputStream(assets.open(assetPath)).use { input -> + val destFile = Environment.TEMPLATES_DIR.resolve(TEMPLATE_CORE_ARCHIVE) + destFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + + AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { val assetPath = ToolsManager.getCommonAsset("${AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME}.br") diff --git a/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt b/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt index a94e4cfe06..880410d5d3 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt @@ -15,6 +15,7 @@ import org.adfa.constants.DOCUMENTATION_DB import org.adfa.constants.GRADLE_API_NAME_JAR_ZIP import org.adfa.constants.GRADLE_DISTRIBUTION_ARCHIVE_NAME import org.adfa.constants.LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME +import org.adfa.constants.TEMPLATE_CORE_ARCHIVE import org.slf4j.LoggerFactory import java.io.File import java.io.FileNotFoundException @@ -72,7 +73,14 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() { logger.debug("Completed extracting '{}' to dir: {}", entry.name, destDir) } - AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { + TEMPLATE_CORE_ARCHIVE -> { + val coreCgt = Environment.TEMPLATES_DIR.resolve(TEMPLATE_CORE_ARCHIVE) + coreCgt.outputStream().use { output -> + zipInput.copyTo(output) + } + } + + AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { logger.debug("Extracting 'bootstrap.zip' to dir: {}", stagingDir) val result = retryOnceOnNoSuchFile( @@ -194,6 +202,7 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() { LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME -> 215389106L AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> 456462823L GRADLE_API_NAME_JAR_ZIP -> 46758608L + TEMPLATE_CORE_ARCHIVE -> 702001L else -> 0L } @@ -203,6 +212,7 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() { ANDROID_SDK_ZIP -> Environment.ANDROID_HOME LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME -> Environment.LOCAL_MAVEN_DIR GRADLE_API_NAME_JAR_ZIP -> Environment.GRADLE_GEN_JARS + TEMPLATE_CORE_ARCHIVE -> Environment.TEMPLATES_DIR else -> throw IllegalStateException("Entry '$entryName' is not expected to be an archive") } diff --git a/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt b/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt index 90b65da21a..2c3d803607 100644 --- a/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt +++ b/composite-builds/build-deps-common/constants/src/main/java/org/adfa/constants/constants.kt @@ -79,4 +79,5 @@ const val GRADLE_API_NAME_JAR_BR = "${GRADLE_API_NAME_JAR}.br" // Templates archive const val TEMPLATE_ARCHIVE_EXTENSION = "cgt" -const val TEMPLATE_CORE_ARCHIVE = "core$TEMPLATE_ARCHIVE_EXTENSION" \ No newline at end of file +const val TEMPLATE_CORE_ARCHIVE = "core.$TEMPLATE_ARCHIVE_EXTENSION" +const val TEMPLATE_CORE_ARCHIVE_BR = "${TEMPLATE_CORE_ARCHIVE}.br" From 11900455d46d9353ad02b4f164dc1d9c2ae17ccf Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 13 Mar 2026 21:59:44 +0800 Subject: [PATCH 05/25] cleanup of widget references and zip issues --- .../templates/impl/TemplateProviderImpl.kt | 5 +- .../androidide/templates/impl/zip/ZipJson.kt | 45 ------- .../templates/impl/zip/ZipRecipeExecutor.kt | 118 +++++++++--------- .../templates/impl/zip/ZipTemplateReader.kt | 92 ++++++-------- 4 files changed, 104 insertions(+), 156 deletions(-) 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 c5a24d031d..31e5bf6e9b 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 @@ -51,7 +51,6 @@ class TemplateProviderImpl : ITemplateProvider { } private fun initializeTemplates() { - // val folder = File("/sdcard/Download/templates") val folder = TEMPLATES_DIR log.debug("Template listing archives in: ${folder.toString()} with extension: ${TEMPLATE_ARCHIVE_EXTENSION}") val list = folder.listFiles { file -> file.extension == TEMPLATE_ARCHIVE_EXTENSION } ?: return @@ -73,8 +72,8 @@ class TemplateProviderImpl : ITemplateProvider { } log.debug("templates: $templates") - } catch (e: Throwable) { - e.printStackTrace() + } catch (e: Exception) { + log.error("Failed to load template from archive: $zipFile", e) } } } diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt index 1a644cb138..5fea15518f 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt @@ -1,21 +1,9 @@ package com.itsaky.androidide.templates.impl.zip -import com.itsaky.androidide.templates.BooleanParameterBuilder -import com.itsaky.androidide.templates.CheckBoxWidget -import com.itsaky.androidide.templates.StringParameterBuilder -import com.itsaky.androidide.templates.TextFieldWidget -import com.itsaky.androidide.templates.Widget data class TemplatesIndex(val templates: List) data class TemplateRef(val path: String, val experimental: Boolean = false,) -// data class TemplateJson( -// val name: String, -// val description: String?, -// val widgets: List = emptyList(), -// val language: Boolean? = true -// ) - data class TemplateJson( val name: String, val description: String?, @@ -73,36 +61,3 @@ data class CheckboxParameterJson( val default: Boolean? = null ) -data class WidgetJson( - val type: String, - val name: String, - val id: String, - val default: Any?, - val options: List?, - val constraints: List? -) { - fun toWidget(): Widget<*>? { - return when(type) { - "string" -> { - val param = StringParameterBuilder().apply { - //this.name = this@WidgetJson.name - //this.description = null - //this.default = this@WidgetJson.default?.toString() ?: "" - //this.key = this@WidgetJson.id - }.build() - TextFieldWidget(param) - } - "boolean" -> { - val param = BooleanParameterBuilder().apply { - //this.name = this@WidgetJson.name - this.description = null - this.default = this@WidgetJson.default?.toString()?.toBoolean() ?: false - //this.key = this@WidgetJson.id - }.build() - CheckBoxWidget(param) - } - // Add more mappings as needed - else -> null - } - } -} 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 1626888f67..987979e994 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 @@ -41,63 +41,69 @@ class ZipRecipeExecutor( ): ProjectTemplateRecipeResult { log.debug("executor called!!") - val zip = zipProvider() - val projectDir = data.projectDir - if (projectDir.exists()) { - return ProjectTemplateRecipeResultImpl(data) - } + zipProvider().use { zip -> + + val projectDir = data.projectDir + if (projectDir.exists()) { + return ProjectTemplateRecipeResultImpl(data) + } - 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 (params, warnings) = metaJson.pebbleParams(data, defModule) - log.debug("params warnings: $warnings") - - log.debug("defModule: $defModule") - - 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.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 relativePath = entry.name.removePrefix("$basePath/") - .replace(packageName.value, defModule.packageName.replace(".", "/")) - - val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)) - - if (entry.isDirectory) { - outFile.mkdirs() - } else { - outFile.parentFile?.mkdirs() - val content = zip.getInputStream(entry).bufferedReader().readText() - - zip.getInputStream(entry).use { input -> - FileOutputStream(outFile).use { output -> - if (entry.name.endsWith(TEMPLATE_EXTENSION)) { - log.debug("template processing ${entry.name}") - val template = pebbleEngine.getTemplate(content) - val writer = StringWriter() - template.evaluate(writer, params) - outFile.writeText(writer.toString(), Charsets.UTF_8) - } else { - input.copyTo(output) + 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 (params, warnings) = metaJson.pebbleParams(data, defModule) + log.debug("params warnings: $warnings") + + log.debug("defModule: $defModule") + + 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.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 relativePath = entry.name.removePrefix("$basePath/") + .replace(packageName.value, defModule.packageName.replace(".", "/")) + + val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)) + + if (entry.isDirectory) { + outFile.mkdirs() + } else { + outFile.parentFile?.mkdirs() + val content = zip.getInputStream(entry).bufferedReader().readText() + + zip.getInputStream(entry).use { input -> + FileOutputStream(outFile).use { output -> + if (entry.name.endsWith(TEMPLATE_EXTENSION)) { + log.debug("template processing ${entry.name}") + val template = pebbleEngine.getTemplate(content) + val writer = StringWriter() + template.evaluate(writer, params) + outFile.writeText(writer.toString(), Charsets.UTF_8) + } else { + input.copyTo(output) + } } } } 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 5a8cb7b2de..29ac71ee22 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 @@ -21,71 +21,66 @@ object ZipTemplateReader { fun read( zipFile: File, - recipeFactory: (TemplateJson, /* List>?, */ String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe + recipeFactory: (TemplateJson, String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe ): List { val templates = mutableListOf() try { - val zip = ZipFile(zipFile) + ZipFile(zipFile).use { zip -> - log.debug("zipFile: $zipFile zip: $zip") + log.debug("zipFile: $zipFile zip: $zip") - val indexEntry = zip.getEntry(ARCHIVE_JSON) ?: return emptyList() - val indexJson = zip.getInputStream(indexEntry).bufferedReader().use { - gson.fromJson(it, TemplatesIndex::class.java) - } + val indexEntry = zip.getEntry(ARCHIVE_JSON) ?: return emptyList() + val indexJson = zip.getInputStream(indexEntry).bufferedReader().use { + gson.fromJson(it, TemplatesIndex::class.java) + } - log.debug("indexJson: $indexJson") - for (templateRef in indexJson.templates) { - try { - val basePath = templateRef.path - log.debug("basePath: $basePath") - val metaEntry = zip.getEntry("$basePath/$META_FOLDER/$META_JSON") ?: continue + log.debug("indexJson: $indexJson") + for (templateRef in indexJson.templates) { + try { + val basePath = templateRef.path + log.debug("basePath: $basePath") + val metaEntry = zip.getEntry("$basePath/$META_FOLDER/$META_JSON") ?: continue - val metaJsonString = zip.getInputStream(metaEntry).bufferedReader().use { reader -> - reader.readText() - } + val metaJsonString = zip.getInputStream(metaEntry).bufferedReader().use { reader -> + reader.readText() + } - val metaJson = gson.fromJson(metaJsonString, TemplateJson::class.java) + val metaJson = gson.fromJson(metaJsonString, TemplateJson::class.java) - log.debug("metaJson: $metaJson") + log.debug("metaJson: $metaJson") - val thumbEntry = zip.getEntry("$basePath/$META_FOLDER/$META_THUMBNAIL") - val thumbData = thumbEntry?.let { zip.getInputStream(it).use { s -> s.readBytes() } } + val thumbEntry = zip.getEntry("$basePath/$META_FOLDER/$META_THUMBNAIL") + val thumbData = thumbEntry?.let { zip.getInputStream(it).use { s -> s.readBytes() } } - if (thumbData == null) log.error("template $basePath/$META_FOLDER/$META_THUMBNAIL not found or is invalid") - log.debug("thumbData: $thumbData") + if (thumbData == null) log.error("template $basePath/$META_FOLDER/$META_THUMBNAIL not found or is invalid") + log.debug("thumbData: $thumbData") - // val widgets: List> = metaJson.widgets.mapNotNull { it.toWidget() } - // val widgets: List> = widgetsEntry?.let { - // val widgetsText = zip.getInputStream(it).bufferedReader().use { r -> r.readText() } - // parseWidgetsFromJson(widgetsText) - // } ?: metaJson.widgets.mapNotNull { it.toWidget() } + val project = baseZipProject( + showLanguage = (metaJson.parameters?.optional?.language != null), + showMinSdk = (metaJson.parameters?.optional?.minsdk != null) + ) { - val project = baseZipProject ( - showLanguage = (metaJson.parameters?.optional?.language != null), - showMinSdk = (metaJson.parameters?.optional?.minsdk != null) - ) { + this.templateNameStr = metaJson.name + this.thumbData = thumbData - this.templateNameStr = metaJson.name - this.thumbData = thumbData + this.templateName = 0 + this.thumb = R.drawable.template_no_activity - this.templateName = 0 - this.thumb = R.drawable.template_no_activity + log.debug("this.name: ${this.templateNameStr}") + this.recipe = TemplateRecipe { executor -> + val innerRecipe = recipeFactory(metaJson, basePath, data, defModule) + innerRecipe.execute(executor) + } + } - log.debug("this.name: ${this.templateNameStr}") - this.recipe = TemplateRecipe { executor -> - val innerRecipe = recipeFactory(metaJson, basePath, /* params*/ data, defModule) - innerRecipe.execute(executor) + log.debug("adding project ${metaJson.name}") + templates.add(project) + } catch (e: Exception) { + log.error("Failed to load template at ${templateRef.path}", e) } } - - log.debug("adding project ${metaJson.name}") - templates.add(project) - } catch (e: Exception) { - log.error("Failed to load template at ${templateRef.path}", e) - } } } catch (e: Exception) { log.error("Failed to read zip file $zipFile", e) @@ -95,11 +90,4 @@ object ZipTemplateReader { return templates } - - private fun parseWidgetsFromJson(jsonText: String): List> { - val type = object : TypeToken>() {}.type - val widgetJsons: List = gson.fromJson(jsonText, type) - return widgetJsons.mapNotNull { it.toWidget() } - } - } From 5b393293c232eb77b398cf187e400a06f159cbe8 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 13 Mar 2026 22:48:34 +0800 Subject: [PATCH 06/25] cleanup imports --- .../itsaky/androidide/templates/impl/TemplateProviderImpl.kt | 1 - .../itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt | 2 -- 2 files changed, 3 deletions(-) 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 31e5bf6e9b..b537bd50bd 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 @@ -28,7 +28,6 @@ import org.adfa.constants.TEMPLATE_ARCHIVE_EXTENSION import com.itsaky.androidide.utils.Environment.TEMPLATES_DIR import org.slf4j.LoggerFactory -import java.io.File import java.util.zip.ZipFile /** 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 29ac71ee22..14d4466d38 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 @@ -1,14 +1,12 @@ package com.itsaky.androidide.templates.impl.zip import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.itsaky.androidide.templates.ModuleTemplateData import com.itsaky.androidide.templates.ProjectTemplate import com.itsaky.androidide.templates.ProjectTemplateData import com.itsaky.androidide.templates.ProjectTemplateRecipeResult import com.itsaky.androidide.templates.R import com.itsaky.androidide.templates.TemplateRecipe -import com.itsaky.androidide.templates.Widget import com.itsaky.androidide.templates.base.baseZipProject import org.slf4j.LoggerFactory import java.io.File From abae382e7c568d4e5813f5d7068632277e0c962e Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Sat, 14 Mar 2026 15:56:13 +0800 Subject: [PATCH 07/25] take out null checks --- .../androidide/templates/impl/TemplateProviderImpl.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 b537bd50bd..e2bbeed323 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 @@ -61,13 +61,9 @@ class TemplateProviderImpl : ITemplateProvider { ZipRecipeExecutor({ ZipFile(zipFile) }, json, path, /* params */ data, defModule) } - if (zipTemplates != null) { - for (t in zipTemplates) { - log.debug("template: $t") - if (t != null) { - templates[t.templateId] = t - } - } + for (t in zipTemplates) { + log.debug("template: $t") + templates[t.templateId] = t } log.debug("templates: $templates") From dafe7159007457a3e4cb0d12f8968457437bba69 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Sat, 14 Mar 2026 16:20:08 +0800 Subject: [PATCH 08/25] restore leak canary --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 44f14d5bc1..625ab1f2be 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,7 +198,7 @@ configurations.matching { it.name.contains("AndroidTest") }.configureEach { } dependencies { - //debugImplementation(libs.common.leakcanary) + debugImplementation(libs.common.leakcanary) // Annotation processors kapt(libs.common.glide.ap) From ab0c3e041df9bfb7882cab954e574d4b1d0f716a Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Sat, 14 Mar 2026 16:24:05 +0800 Subject: [PATCH 09/25] set core.cgt.br expected size --- .../java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt b/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt index 202a9ebf48..bc1a2dc738 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt @@ -229,6 +229,7 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() { AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> 124120151L GRADLE_API_NAME_JAR_ZIP -> 29447748L AssetsInstallationHelper.PLUGIN_ARTIFACTS_ZIP -> 86442L + TEMPLATE_CORE_ARCHIVE -> 133120L else -> 0L } From db56054f3f0f741b37e8d122d03020a5b3396a35 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Sun, 15 Mar 2026 11:02:17 +0800 Subject: [PATCH 10/25] prevent zip slip --- .../templates/impl/zip/ZipRecipeExecutor.kt | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) 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 987979e994..31d959a323 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 @@ -48,6 +48,8 @@ class ZipRecipeExecutor( return ProjectTemplateRecipeResultImpl(data) } + val projectRoot = projectDir.canonicalFile + val customSyntax = Syntax.Builder() .setPrintOpenDelimiter(DELIM_PRINT_OPEN) .setPrintCloseDelimiter(DELIM_PRINT_CLOSE) @@ -85,23 +87,28 @@ class ZipRecipeExecutor( val relativePath = entry.name.removePrefix("$basePath/") .replace(packageName.value, defModule.packageName.replace(".", "/")) - val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)) + 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 + } if (entry.isDirectory) { outFile.mkdirs() } else { outFile.parentFile?.mkdirs() - val content = zip.getInputStream(entry).bufferedReader().readText() - - zip.getInputStream(entry).use { input -> - FileOutputStream(outFile).use { output -> - if (entry.name.endsWith(TEMPLATE_EXTENSION)) { - log.debug("template processing ${entry.name}") - val template = pebbleEngine.getTemplate(content) - val writer = StringWriter() - template.evaluate(writer, params) - outFile.writeText(writer.toString(), Charsets.UTF_8) - } else { + + 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, params) + outFile.writeText(writer.toString(), Charsets.UTF_8) + } else { + zip.getInputStream(entry).use { input -> + outFile.outputStream().use { output -> input.copyTo(output) } } From 1f10d6c7b13d2f897a734f3e6acea3c16ccd5bda Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Sun, 15 Mar 2026 11:02:55 +0800 Subject: [PATCH 11/25] take out unused import --- .../itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt | 1 - 1 file changed, 1 deletion(-) 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 31d959a323..a219cc1990 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 @@ -2,7 +2,6 @@ package com.itsaky.androidide.templates.impl.zip import com.itsaky.androidide.templates.Language import java.io.File -import java.io.FileOutputStream import java.io.StringWriter import java.util.zip.ZipFile From 849efc47580e27c5e00cbb0a2e1bbedfe1ebff42 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Sun, 15 Mar 2026 12:30:44 +0800 Subject: [PATCH 12/25] initial add widgets --- .../templates/impl/zip/ZipTemplateReader.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 14d4466d38..2d77fdc4eb 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 @@ -1,12 +1,17 @@ package com.itsaky.androidide.templates.impl.zip import com.google.gson.Gson +import com.itsaky.androidide.templates.BooleanParameterBuilder +import com.itsaky.androidide.templates.CheckBoxWidget import com.itsaky.androidide.templates.ModuleTemplateData import com.itsaky.androidide.templates.ProjectTemplate import com.itsaky.androidide.templates.ProjectTemplateData import com.itsaky.androidide.templates.ProjectTemplateRecipeResult import com.itsaky.androidide.templates.R +import com.itsaky.androidide.templates.StringParameterBuilder import com.itsaky.androidide.templates.TemplateRecipe +import com.itsaky.androidide.templates.TextFieldWidget +import com.itsaky.androidide.templates.Widget import com.itsaky.androidide.templates.base.baseZipProject import org.slf4j.LoggerFactory import java.io.File @@ -55,6 +60,24 @@ object ZipTemplateReader { if (thumbData == null) log.error("template $basePath/$META_FOLDER/$META_THUMBNAIL not found or is invalid") log.debug("thumbData: $thumbData") + val userWidgets = mutableListOf>() + +// metaJson.parameters?.user?.text?.forEach { textParam -> +// val param = StringParameterBuilder() +// .name(textParam.label) +// .default(textParam.default ?: "") +// .build() +// userWidgets.add(TextFieldWidget(param)) +// } +// +// metaJson.parameters?.user?.checkbox?.forEach { checkboxParam -> +// val param = BooleanParameterBuilder() +// .name(checkboxParam.label) +// .default(checkboxParam.default ?: false) +// .build() +// userWidgets.add(CheckBoxWidget(param)) +// } + val project = baseZipProject( showLanguage = (metaJson.parameters?.optional?.language != null), showMinSdk = (metaJson.parameters?.optional?.minsdk != null) From ec4417fd61bea11ac690bbdfee62e7cee7fc5d0c Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Mon, 16 Mar 2026 23:17:57 +0800 Subject: [PATCH 13/25] user defined parameters --- app/build.gradle.kts | 12 +-- .../itsaky/androidide/templates/parameters.kt | 39 ++++++--- .../templates/impl/TemplateProviderImpl.kt | 4 +- .../impl/TemplateWidgetViewProviderImpl.kt | 12 ++- .../templates/impl/zip/ZipRecipeExecutor.kt | 79 ++++++++++++++----- .../templates/impl/zip/ZipTemplateReader.kt | 51 +++++++----- 6 files changed, 138 insertions(+), 59 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 625ab1f2be..fe8b53842c 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1118,12 +1118,12 @@ val releaseAssets = "v8/bootstrap.zip.br", "release", ), - Asset( - "assets/release/common/data/common/core.cgt.br", - "https://appdevforall.org/dev-assets/release/core.cgt.br", - "core.cgt.br", - "release", - ), + Asset( + "assets/release/common/data/common/core.cgt.br", + "https://appdevforall.org/dev-assets/release/core.cgt.br", + "core.cgt.br", + "release", + ), ) fun assetsBatch( diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt index f8ec1fd075..f96589df70 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/parameters.kt @@ -86,7 +86,9 @@ abstract class Parameter( @StringRes val name: Int, @StringRes val description: Int?, val default: T, val tooltipTag: String? = null, - var constraints: List + var constraints: List, + var id: Int? = null, + val nameStr: String? = null ) { private val observers = hashSetOf>() @@ -252,8 +254,12 @@ abstract class ParameterBuilder { var constraints: List = emptyList() + var id: Int? = null + var nameStr: String? = null + protected open fun validate() { - checkNotNull(name) { "Parameter must have a name" } + val nameAll: Any? = if (name != null) name else nameStr + checkNotNull(nameAll) { "Parameter must have a name" } checkNotNull(default) { "Parameter must have a default value" } } @@ -262,13 +268,14 @@ abstract class ParameterBuilder { class BooleanParameter( @StringRes name: Int, @StringRes description: Int?, - default: Boolean, tooltipTag: String?, constraints: List -) : Parameter(name, description, default, tooltipTag, constraints) + default: Boolean, tooltipTag: String?, constraints: List, + id: Int? = null, nameStr: String? = null +) : Parameter(name, description, default, tooltipTag, constraints, id, nameStr) class BooleanParameterBuilder : ParameterBuilder() { override fun build(): BooleanParameter { - return BooleanParameter(name!!, description, default!!, tooltipTag, constraints) + return BooleanParameter(name!!, description, default!!, tooltipTag, constraints, id, nameStr) } } @@ -292,8 +299,9 @@ abstract class TextFieldParameter( val onEndIconClick: View.OnClickListener?, val inputType: Int?, @StyleableRes val imeOptions: Int?, - val maxLines: Int?, tooltipTag: String?, constraints: List -) : Parameter(name, description, default, tooltipTag, constraints) + val maxLines: Int?, tooltipTag: String?, constraints: List, + id: Int?, nameStr: String? +) : Parameter(name, description, default, tooltipTag, constraints, id, nameStr) abstract class TextFieldParameterBuilder( var startIcon: ((TextFieldParameter) -> Int)? = null, @@ -302,7 +310,7 @@ abstract class TextFieldParameterBuilder( var onEndIconClick: View.OnClickListener? = null, var inputType: Int? = null, var imeOptions: Int? = null, - var maxLines: Int? = null + var maxLines: Int? = null, ) : ParameterBuilder() class StringParameter( @@ -316,10 +324,13 @@ class StringParameter( @StyleableRes imeOptions: Int? = null, maxLines: Int? = null, tooltipTag: String?, - constraints: List + constraints: List, + id: Int?, + nameStr: String? ) : TextFieldParameter( name, description, default, startIcon, endIcon, - onStartIconClick, onEndIconClick, inputType, imeOptions, maxLines, tooltipTag, constraints + onStartIconClick, onEndIconClick, inputType, imeOptions, maxLines, tooltipTag, constraints, + id, nameStr ) class StringParameterBuilder : TextFieldParameterBuilder() { @@ -338,6 +349,8 @@ class StringParameterBuilder : TextFieldParameterBuilder() { maxLines = maxLines, tooltipTag = tooltipTag, constraints = constraints, + id = id, + nameStr = nameStr ) } } @@ -351,10 +364,12 @@ class EnumParameter>( onEndIconClick: View.OnClickListener?, tooltipTag: String?, constraints: List, val displayName: ((T) -> String)? = null, - val filter: ((T) -> Boolean)? = null + val filter: ((T) -> Boolean)? = null, + id: Int? = null, nameStr: String? = null ) : TextFieldParameter( name, description, default, startIcon, endIcon, onStartIconClick, - onEndIconClick, null, null, null, tooltipTag, constraints + onEndIconClick, null, null, null, tooltipTag, constraints, + id, nameStr ) { /** 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 e2bbeed323..f458479688 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 @@ -57,8 +57,8 @@ class TemplateProviderImpl : ITemplateProvider { for (zipFile in list) { log.debug("Template archive: $zipFile") try { - val zipTemplates = ZipTemplateReader.read(zipFile) { json, path, /* params */ data, defModule -> - ZipRecipeExecutor({ ZipFile(zipFile) }, json, path, /* params */ data, defModule) + val zipTemplates = ZipTemplateReader.read(zipFile) { json, params, path, data, defModule -> + ZipRecipeExecutor({ ZipFile(zipFile) }, json, params, path, data, defModule) } for (t in zipTemplates) { diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt index 5b3a136e24..e83334c1cf 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt @@ -114,7 +114,11 @@ class TemplateWidgetViewProviderImpl : ITemplateWidgetViewProvider { private fun createCheckBox(context: Context, widget: CheckBoxWidget): View { return LayoutCheckboxBinding.inflate(LayoutInflater.from(context)).apply { val param = widget.parameter as BooleanParameter - root.setText(param.name) + if (param.nameStr != null) { + root.text = param.nameStr + } else { + root.setText(param.name) + } root.isChecked = param.value val observer = object : DefaultObserver() { @@ -327,7 +331,11 @@ class TemplateWidgetViewProviderImpl : ITemplateWidgetViewProvider { root: TextInputLayout, crossinline onTextChanged: (String) -> Unit ) { - root.setHint(name) + if (nameStr != null) { + root.hint = nameStr + } else { + root.setHint(name) + } resetStartAndEndIcons(context, root) root.editText!!.addTextChangedListener(object : SingleTextWatcher() { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 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 a219cc1990..756aaec65a 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 @@ -11,6 +11,7 @@ import io.pebbletemplates.pebble.loader.StringLoader import io.pebbletemplates.pebble.lexer.Syntax import com.itsaky.androidide.templates.ModuleTemplateData +import com.itsaky.androidide.templates.Parameter import com.itsaky.androidide.templates.ProjectTemplateData import com.itsaky.androidide.templates.ProjectTemplateRecipeResult import com.itsaky.androidide.templates.RecipeExecutor @@ -25,13 +26,14 @@ import org.adfa.constants.Sdk class ZipRecipeExecutor( private val zipProvider: () -> ZipFile, private val metaJson: TemplateJson, + private val params: MutableMap>, private val basePath: String, private val data: ProjectTemplateData, private val defModule: ModuleTemplateData, ) : TemplateRecipe { companion object { - + const val ZIP_SEPARATOR = "/" private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java) } @@ -40,14 +42,25 @@ class ZipRecipeExecutor( ): ProjectTemplateRecipeResult { log.debug("executor called!!") - zipProvider().use { zip -> - val projectDir = data.projectDir - if (projectDir.exists()) { - return ProjectTemplateRecipeResultImpl(data) - } + //log.debug("params:") + //params.forEach { (identifier, param) -> + // log.debug("identifier: $identifier, name=${param.name}, default=${param.default}, value=${param.value}") + //} + + val projectDir = data.projectDir + if (projectDir.exists()) { + return ProjectTemplateRecipeResultImpl(data) + } - val projectRoot = projectDir.canonicalFile + val projectRoot = projectDir.canonicalFile + + val flags: Map = + params.mapNotNull { (identifier, param) -> + (param.value as? Boolean)?.let { identifier to it } + }.toMap() + + zipProvider().use { zip -> val customSyntax = Syntax.Builder() .setPrintOpenDelimiter(DELIM_PRINT_OPEN) @@ -63,10 +76,10 @@ class ZipRecipeExecutor( .syntax(customSyntax) .build() - val (params, warnings) = metaJson.pebbleParams(data, defModule) - log.debug("params warnings: $warnings") + val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params) + log.debug("identifiers warnings: $warnings") - log.debug("defModule: $defModule") + //log.debug("defModule: $defModule") val packageName = resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) @@ -83,7 +96,10 @@ class ZipRecipeExecutor( ) ) continue - val relativePath = entry.name.removePrefix("$basePath/") + val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue + + // val relativePath = entry.name.removePrefix("$basePath/") + val relativePath = normalized.removePrefix("$basePath/") .replace(packageName.value, defModule.packageName.replace(".", "/")) val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile @@ -103,7 +119,7 @@ class ZipRecipeExecutor( val content = zip.getInputStream(entry).bufferedReader().use { it.readText() } val template = pebbleEngine.getTemplate(content) val writer = StringWriter() - template.evaluate(writer, params) + template.evaluate(writer, identifiers) outFile.writeText(writer.toString(), Charsets.UTF_8) } else { zip.getInputStream(entry).use { input -> @@ -147,15 +163,16 @@ class ZipRecipeExecutor( } } - fun safeLanguageName(language: Language?): String = + private fun safeLanguageName(language: Language?): String = language?.name?.lowercase() ?: "" - fun safeMinSdkApi(minSdk: Sdk?): String = + private fun safeMinSdkApi(minSdk: Sdk?): String = minSdk?.api?.toString() ?: "" - fun TemplateJson.pebbleParams( + private fun TemplateJson.pebbleParams( data: ProjectTemplateData, - defModule: ModuleTemplateData + defModule: ModuleTemplateData, + params: MutableMap> ): Pair, List> { val warnings = mutableListOf() @@ -199,7 +216,7 @@ class ZipRecipeExecutor( val javaTarget = resolveString(system?.javaTarget?.identifier, KEY_JAVA_TARGET) if (javaTarget.usedDefault) warnings += "Missing 'javaTarget', defaulted to $KEY_JAVA_TARGET" - val map = mapOf( + val baseMap = mapOf( appName.value to data.name, packageName.value to defModule.packageName, saveLocation.value to data.projectDir.toString(), @@ -215,6 +232,10 @@ class ZipRecipeExecutor( javaTarget.value to defModule.versions.javaTarget ) + val map = baseMap + params.mapValues { (_, param) -> + param.value ?: "" + } + return map to warnings } @@ -223,14 +244,34 @@ class ZipRecipeExecutor( val usedDefault: Boolean ) - fun resolveString(value: String?, default: String): ResolvedParam { + private fun resolveString(value: String?, default: String): ResolvedParam { return if (value.isNullOrBlank()) ResolvedParam(default, true) else ResolvedParam(value, false) } - fun resolveBoolean(raw: Boolean?, default: Boolean): ResolvedParam { + private fun resolveBoolean(raw: Boolean?, default: Boolean): ResolvedParam { return if (raw == null) ResolvedParam(default, true) else ResolvedParam(raw, false) } + private fun filterAndNormalizeZipEntry( + entryName: String, + flags: Map + ): String? { + val parts = entryName.split(ZIP_SEPARATOR).filter { it.isNotEmpty() } + if (parts.isEmpty()) return null + + val normalizedParts = mutableListOf() + + for (part in parts) { + when (flags[part]) { + null -> normalizedParts.add(part) + true -> { } + false -> return null + } + } + + return normalizedParts.joinToString(ZIP_SEPARATOR) + } + } 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 2d77fdc4eb..850082ffce 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 @@ -4,6 +4,7 @@ import com.google.gson.Gson import com.itsaky.androidide.templates.BooleanParameterBuilder import com.itsaky.androidide.templates.CheckBoxWidget import com.itsaky.androidide.templates.ModuleTemplateData +import com.itsaky.androidide.templates.Parameter import com.itsaky.androidide.templates.ProjectTemplate import com.itsaky.androidide.templates.ProjectTemplateData import com.itsaky.androidide.templates.ProjectTemplateRecipeResult @@ -13,6 +14,9 @@ import com.itsaky.androidide.templates.TemplateRecipe import com.itsaky.androidide.templates.TextFieldWidget import com.itsaky.androidide.templates.Widget import com.itsaky.androidide.templates.base.baseZipProject +import com.itsaky.androidide.templates.R.string +import com.itsaky.androidide.templates.booleanParameter +import com.itsaky.androidide.templates.stringParameter import org.slf4j.LoggerFactory import java.io.File import java.util.zip.ZipFile @@ -24,7 +28,7 @@ object ZipTemplateReader { fun read( zipFile: File, - recipeFactory: (TemplateJson, String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe + recipeFactory: (TemplateJson, MutableMap>, String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe ): List { val templates = mutableListOf() @@ -61,22 +65,30 @@ object ZipTemplateReader { log.debug("thumbData: $thumbData") val userWidgets = mutableListOf>() + val params = mutableMapOf>() -// metaJson.parameters?.user?.text?.forEach { textParam -> -// val param = StringParameterBuilder() -// .name(textParam.label) -// .default(textParam.default ?: "") -// .build() -// userWidgets.add(TextFieldWidget(param)) -// } -// -// metaJson.parameters?.user?.checkbox?.forEach { checkboxParam -> -// val param = BooleanParameterBuilder() -// .name(checkboxParam.label) -// .default(checkboxParam.default ?: false) -// .build() -// userWidgets.add(CheckBoxWidget(param)) -// } + metaJson.parameters?.user?.text?.forEach { textParam -> + val param = stringParameter { + // name = string.project_app_name + name = 0 + nameStr = textParam.label ?: "" + default = textParam.default ?: "" + + } + userWidgets.add(TextFieldWidget(param)) + params[textParam.identifier] = param + } + + metaJson.parameters?.user?.checkbox?.forEach { checkboxParam -> + val param = booleanParameter { + // name = string.project_app_name + name = 0 + nameStr = checkboxParam.label ?: "" + default = checkboxParam.default ?: false + } + userWidgets.add(CheckBoxWidget(param)) + params[checkboxParam.identifier] = param + } val project = baseZipProject( showLanguage = (metaJson.parameters?.optional?.language != null), @@ -89,9 +101,13 @@ object ZipTemplateReader { this.templateName = 0 this.thumb = R.drawable.template_no_activity + for (widget in userWidgets) { + widgets(widget) + } + log.debug("this.name: ${this.templateNameStr}") this.recipe = TemplateRecipe { executor -> - val innerRecipe = recipeFactory(metaJson, basePath, data, defModule) + val innerRecipe = recipeFactory(metaJson, params, basePath, data, defModule) innerRecipe.execute(executor) } } @@ -105,7 +121,6 @@ object ZipTemplateReader { } } catch (e: Exception) { log.error("Failed to read zip file $zipFile", e) - // return emptyList() } return templates From 63a428da8b2f79df5ec223ea1c2b2f6b78bab446 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Mon, 16 Mar 2026 23:20:57 +0800 Subject: [PATCH 14/25] unused imports --- .../templates/impl/TemplateWidgetViewProviderImpl.kt | 1 - .../itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt | 3 --- 2 files changed, 4 deletions(-) diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt index e83334c1cf..c5ff6a5ec0 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateWidgetViewProviderImpl.kt @@ -19,7 +19,6 @@ package com.itsaky.androidide.templates.impl import android.annotation.SuppressLint import android.content.Context -import android.graphics.drawable.ColorDrawable import android.util.TypedValue import android.view.LayoutInflater import android.view.MotionEvent 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 850082ffce..a445b57c47 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 @@ -1,7 +1,6 @@ package com.itsaky.androidide.templates.impl.zip import com.google.gson.Gson -import com.itsaky.androidide.templates.BooleanParameterBuilder import com.itsaky.androidide.templates.CheckBoxWidget import com.itsaky.androidide.templates.ModuleTemplateData import com.itsaky.androidide.templates.Parameter @@ -9,12 +8,10 @@ import com.itsaky.androidide.templates.ProjectTemplate import com.itsaky.androidide.templates.ProjectTemplateData import com.itsaky.androidide.templates.ProjectTemplateRecipeResult import com.itsaky.androidide.templates.R -import com.itsaky.androidide.templates.StringParameterBuilder import com.itsaky.androidide.templates.TemplateRecipe import com.itsaky.androidide.templates.TextFieldWidget import com.itsaky.androidide.templates.Widget import com.itsaky.androidide.templates.base.baseZipProject -import com.itsaky.androidide.templates.R.string import com.itsaky.androidide.templates.booleanParameter import com.itsaky.androidide.templates.stringParameter import org.slf4j.LoggerFactory From 3c89bd7efc8009edec0fbcab4836a26d22953384 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Wed, 18 Mar 2026 16:05:11 +0800 Subject: [PATCH 15/25] modify log entry --- .../itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a445b57c47..945bade0db 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 @@ -33,7 +33,7 @@ object ZipTemplateReader { try { ZipFile(zipFile).use { zip -> - log.debug("zipFile: $zipFile zip: $zip") + log.debug("zipFile: $zipFile") val indexEntry = zip.getEntry(ARCHIVE_JSON) ?: return emptyList() val indexJson = zip.getInputStream(indexEntry).bufferedReader().use { From 02ddbaa4c27173de30c32d516ecbfdfd201c83ed Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Thu, 19 Mar 2026 00:18:08 +0800 Subject: [PATCH 16/25] exclude basepath i.e. template root folder in generated project --- .../itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt | 1 + 1 file changed, 1 insertion(+) 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 756aaec65a..2437fb6c83 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 @@ -86,6 +86,7 @@ class ZipRecipeExecutor( 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) && From 001241e91e3982baa08c2ba20cad0b1a05650274 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Thu, 19 Mar 2026 14:07:58 +0800 Subject: [PATCH 17/25] read tooltipTag from template.json --- .../java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt | 1 + .../itsaky/androidide/templates/impl/zip/ZipTemplateReader.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt index 5fea15518f..806e87db5e 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt @@ -8,6 +8,7 @@ data class TemplateJson( val name: String, val description: String?, val version: String?, + val tooltipTag: String = "", val parameters: ParametersJson? = null, val system: SystemParametersJson? = null ) 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 945bade0db..da0d26cfc7 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 @@ -93,6 +93,7 @@ object ZipTemplateReader { ) { this.templateNameStr = metaJson.name + this.tooltipTag = metaJson.tooltipTag this.thumbData = thumbData this.templateName = 0 From 09f3d31955c94d7300c181703fe1f14027f52376 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Thu, 19 Mar 2026 22:43:19 +0800 Subject: [PATCH 18/25] uncomment lib entry for fragment-ktx and take out comments --- .../com/itsaky/androidide/fragments/TemplateDetailsFragment.kt | 2 -- gradle/libs.versions.toml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) 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 b83277ccd4..12eb15047a 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt @@ -136,9 +136,7 @@ class TemplateDetailsFragment : name = result.data.name, createdAt = now, lastModified = now, -// templateName = getString(template.templateName), templateName = template.templateNameStr, -// language = result.data.language.name language = result.data.language?.name ?: "unknown" ) ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a0d9bbae5..30bb172a63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -223,7 +223,7 @@ androidx-vectors = { module = "androidx.vectordrawable:vectordrawable", version. androidx-animated_vectors = { module = "androidx.vectordrawable:vectordrawable-animated", version.ref = "androidx-vectordrawable" } androidx-core = { module = "androidx.core:core", version = "1.13.1" } androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.13.1" } -#androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.6.2" } +androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.6.2" } androidx-libDesugaring = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } androidx-transition = { module = "androidx.transition:transition-ktx", version = "1.5.1" } From 69bd3c16e78fd01d9d7f293e5da02c1752a68b64 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 19:51:17 +0800 Subject: [PATCH 19/25] use File.separator instead of declaring a constant ZIP_SEPARATOR and remove extraneous comments --- .../templates/impl/zip/ZipRecipeExecutor.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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 2437fb6c83..866c90035d 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 @@ -33,7 +33,6 @@ class ZipRecipeExecutor( ) : TemplateRecipe { companion object { - const val ZIP_SEPARATOR = "/" private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java) } @@ -43,11 +42,6 @@ class ZipRecipeExecutor( log.debug("executor called!!") - //log.debug("params:") - //params.forEach { (identifier, param) -> - // log.debug("identifier: $identifier, name=${param.name}, default=${param.default}, value=${param.value}") - //} - val projectDir = data.projectDir if (projectDir.exists()) { return ProjectTemplateRecipeResultImpl(data) @@ -79,8 +73,6 @@ class ZipRecipeExecutor( val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params) log.debug("identifiers warnings: $warnings") - //log.debug("defModule: $defModule") - val packageName = resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) @@ -259,7 +251,7 @@ class ZipRecipeExecutor( entryName: String, flags: Map ): String? { - val parts = entryName.split(ZIP_SEPARATOR).filter { it.isNotEmpty() } + val parts = entryName.split(File.separator).filter { it.isNotEmpty() } if (parts.isEmpty()) return null val normalizedParts = mutableListOf() @@ -272,7 +264,7 @@ class ZipRecipeExecutor( } } - return normalizedParts.joinToString(ZIP_SEPARATOR) + return normalizedParts.joinToString(File.separator) } } From 862adcf25f7959c9176dddd52df0c56360f0179f Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 20:38:29 +0800 Subject: [PATCH 20/25] readable warnings log --- .../itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 866c90035d..3f00cdaa08 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 @@ -71,7 +71,7 @@ class ZipRecipeExecutor( .build() val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params) - log.debug("identifiers warnings: $warnings") + log.debug("identifiers warnings: ${warnings.joinToString(System.lineSeparator())}") val packageName = resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) @@ -91,7 +91,6 @@ class ZipRecipeExecutor( val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue - // val relativePath = entry.name.removePrefix("$basePath/") val relativePath = normalized.removePrefix("$basePath/") .replace(packageName.value, defModule.packageName.replace(".", "/")) From 41812d801d225aab3de93aa9f87962e7df7654ca Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 21:09:59 +0800 Subject: [PATCH 21/25] proguard rules to prevent obfuscation of gson used classes --- templates-impl/proguard-rules.pro | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/templates-impl/proguard-rules.pro b/templates-impl/proguard-rules.pro index 481bb43481..518cb9ec1d 100644 --- a/templates-impl/proguard-rules.pro +++ b/templates-impl/proguard-rules.pro @@ -18,4 +18,12 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keep class com.itsaky.androidide.templates.impl.zip.** { *; } + +-keepattributes Signature +-keepattributes *Annotation* + +-keepattributes InnerClasses +-keepattributes EnclosingMethod From 80fe0df50fda090ded74e1219ac5dca462f23134 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 21:19:37 +0800 Subject: [PATCH 22/25] clean out extraneous debugging lines --- .../java/com/itsaky/androidide/adapters/TemplateListAdapter.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt index 93baea093d..778be2f930 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt @@ -74,11 +74,8 @@ class TemplateListAdapter( return@apply } - log.debug("template: $template") templateName.text = template.templateNameStr - log.debug("text: ${template.templateNameStr} templateName.text: ${templateName.text}") if (template.thumbData != null) { - log.debug("thumbData is not null") Glide.with(templateIcon.context) .asBitmap() .load(template.thumbData) From 9cb3fedb60fcdf6597c3547104fa77e7afb9d539 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 21:32:36 +0800 Subject: [PATCH 23/25] remove unnecessary log object --- .../java/com/itsaky/androidide/adapters/TemplateListAdapter.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt index 778be2f930..f777a620cc 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt @@ -39,9 +39,6 @@ class TemplateListAdapter( private val onClick: ((Template<*>, ViewHolder) -> Unit)? = null, private val onLongClick: ((Template<*>, View) -> Unit)? = null, ) : RecyclerView.Adapter() { - companion object { - private val log = LoggerFactory.getLogger(TemplateListAdapter::class.java) - } private val templates = templates.toMutableList() From e2725fde178ad8a004f2326bb89f1ebdb0e13abb Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 22:59:12 +0800 Subject: [PATCH 24/25] formatting --- app/build.gradle.kts | 14 +- .../adapters/TemplateListAdapter.kt | 24 +- .../assets/AssetsInstallationHelper.kt | 2 +- .../assets/BundledAssetsInstaller.kt | 48 +-- .../androidide/assets/SplitAssetsInstaller.kt | 18 +- .../fragments/TemplateDetailsFragment.kt | 1 - templates-api/build.gradle.kts | 2 +- .../templates/base/ProjectTemplateBuilder.kt | 4 +- .../itsaky/androidide/templates/base/base.kt | 177 ++++----- templates-impl/build.gradle.kts | 4 +- .../templates/impl/TemplateProviderImpl.kt | 96 +++-- .../androidide/templates/impl/zip/ZipJson.kt | 62 +-- .../templates/impl/zip/ZipRecipeExecutor.kt | 376 +++++++++--------- .../templates/impl/zip/ZipTemplateReader.kt | 200 +++++----- 14 files changed, 510 insertions(+), 518 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe8b53842c..b37f62c183 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -541,7 +541,7 @@ fun createAssetsZip(arch: String) { "documentation.db", bootstrapName, "plugin-artifacts.zip", - "core.cgt" + "core.cgt" ).forEach { fileName -> val filePath = sourceDir.resolve(fileName) if (!filePath.exists()) { @@ -1060,12 +1060,12 @@ val debugAssets = "localMvnRepository.zip", "debug", ), - Asset( - "assets/core.cgt", - "https://appdevforall.org/dev-assets/debug/core.cgt", - "core.cgt", - "debug", - ), + Asset( + "assets/core.cgt", + "https://appdevforall.org/dev-assets/debug/core.cgt", + "core.cgt", + "debug", + ), ) val releaseAssets = diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt index f777a620cc..2b63f48755 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt @@ -27,7 +27,6 @@ import com.google.android.material.shape.CornerFamily import com.itsaky.androidide.adapters.TemplateListAdapter.ViewHolder import com.itsaky.androidide.databinding.LayoutTemplateListItemBinding import com.itsaky.androidide.templates.Template -import org.slf4j.LoggerFactory /** * [RecyclerView.Adapter] for showing templates in a [RecyclerView]. @@ -64,22 +63,23 @@ class TemplateListAdapter( holder: ViewHolder, position: Int, ) { - holder.binding.apply { - val template = templates[position] + + holder.binding.apply { + val template = templates[position] if (template == Template.EMPTY) { root.visibility = View.INVISIBLE return@apply } - templateName.text = template.templateNameStr - if (template.thumbData != null) { - Glide.with(templateIcon.context) - .asBitmap() - .load(template.thumbData) - .into(templateIcon) - } else { - templateIcon.setImageResource(template.thumb) - } + templateName.text = template.templateNameStr + if (template.thumbData != null) { + Glide.with(templateIcon.context) + .asBitmap() + .load(template.thumbData) + .into(templateIcon) + } else { + templateIcon.setImageResource(template.thumb) + } templateIcon.shapeAppearanceModel = templateIcon.shapeAppearanceModel diff --git a/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt b/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt index 7901d7f50c..55b5e21a73 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt @@ -115,7 +115,7 @@ object AssetsInstallationHelper { GRADLE_API_NAME_JAR_ZIP, LLAMA_AAR, PLUGIN_ARTIFACTS_ZIP, - TEMPLATE_CORE_ARCHIVE, + TEMPLATE_CORE_ARCHIVE, ) val stagingDir = Files.createTempDirectory(UUID.randomUUID().toString()) diff --git a/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt b/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt index bc1a2dc738..b69dfd21e1 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/BundledAssetsInstaller.kt @@ -80,36 +80,36 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() { } } - TEMPLATE_CORE_ARCHIVE -> { - val assetPath = ToolsManager.getCommonAsset(TEMPLATE_CORE_ARCHIVE_BR) - BrotliInputStream(assets.open(assetPath)).use { input -> - val destFile = Environment.TEMPLATES_DIR.resolve(TEMPLATE_CORE_ARCHIVE) - destFile.outputStream().use { output -> - input.copyTo(output) - } - } - } - - AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { + TEMPLATE_CORE_ARCHIVE -> { + val assetPath = ToolsManager.getCommonAsset(TEMPLATE_CORE_ARCHIVE_BR) + BrotliInputStream(assets.open(assetPath)).use { input -> + val destFile = Environment.TEMPLATES_DIR.resolve(TEMPLATE_CORE_ARCHIVE) + destFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + + AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { val assetPath = ToolsManager.getCommonAsset("${AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME}.br") val result = retryOnceOnNoSuchFile ( onFirstFailure = { Files.createDirectories(stagingDir) }, onSecondFailure = { e2 -> - throw IOException( - context.getString(R.string.terminal_installation_failed_low_storage), - e2 - ) - } - ) { - withTempZipChannel( - stagingDir = stagingDir, - prefix = "bootstrap", - writeTo = { path -> writeBrotliAssetToPath(context, assetPath, path) }, - useChannel = { ch -> TerminalInstaller.installIfNeeded(context, ch) } - ) - } + throw IOException( + context.getString(R.string.terminal_installation_failed_low_storage), + e2 + ) + } + ) { + withTempZipChannel( + stagingDir = stagingDir, + prefix = "bootstrap", + writeTo = { path -> writeBrotliAssetToPath(context, assetPath, path) }, + useChannel = { ch -> TerminalInstaller.installIfNeeded(context, ch) } + ) + } when (result) { is TerminalInstaller.InstallResult.Success -> {} diff --git a/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt b/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt index 880410d5d3..935b69c585 100644 --- a/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt +++ b/app/src/main/java/com/itsaky/androidide/assets/SplitAssetsInstaller.kt @@ -73,14 +73,14 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() { logger.debug("Completed extracting '{}' to dir: {}", entry.name, destDir) } - TEMPLATE_CORE_ARCHIVE -> { - val coreCgt = Environment.TEMPLATES_DIR.resolve(TEMPLATE_CORE_ARCHIVE) - coreCgt.outputStream().use { output -> - zipInput.copyTo(output) - } - } - - AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { + TEMPLATE_CORE_ARCHIVE -> { + val coreCgt = Environment.TEMPLATES_DIR.resolve(TEMPLATE_CORE_ARCHIVE) + coreCgt.outputStream().use { output -> + zipInput.copyTo(output) + } + } + + AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> { logger.debug("Extracting 'bootstrap.zip' to dir: {}", stagingDir) val result = retryOnceOnNoSuchFile( @@ -212,7 +212,7 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() { ANDROID_SDK_ZIP -> Environment.ANDROID_HOME LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME -> Environment.LOCAL_MAVEN_DIR GRADLE_API_NAME_JAR_ZIP -> Environment.GRADLE_GEN_JARS - TEMPLATE_CORE_ARCHIVE -> Environment.TEMPLATES_DIR + TEMPLATE_CORE_ARCHIVE -> Environment.TEMPLATES_DIR else -> throw IllegalStateException("Entry '$entryName' is not expected to be an archive") } 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 12eb15047a..10b0a5c9fe 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt @@ -163,7 +163,6 @@ class TemplateDetailsFragment : template ?: return binding.widgets.adapter = TemplateWidgetsListAdapter(template.widgets) - //binding.title.setText(template.templateName) binding.title.text = template.templateNameStr } } \ No newline at end of file diff --git a/templates-api/build.gradle.kts b/templates-api/build.gradle.kts index fba5a80cda..a8ca000bc1 100644 --- a/templates-api/build.gradle.kts +++ b/templates-api/build.gradle.kts @@ -40,5 +40,5 @@ dependencies { api(libs.androidx.appcompat) api(libs.google.material) - implementation(libs.google.gson) + implementation(libs.google.gson) } diff --git a/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt b/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt index ac3814ca9b..bed627c668 100644 --- a/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt +++ b/templates-api/src/main/java/com/itsaky/androidide/templates/base/ProjectTemplateBuilder.kt @@ -199,7 +199,7 @@ class ProjectTemplateBuilder : ExecutorDataTemplateBuilder baseFile( * @param block Function to configure the template. */ inline fun baseZipProject( - projectName: StringParameter = projectNameParameter(), - packageName: StringParameter = packageNameParameter(), - useKts: BooleanParameter = useKtsParameter(), - minSdk: EnumParameter = minSdkParameter(), - language: EnumParameter = projectLanguageParameter(), - projectVersionData: ProjectVersionData = ProjectVersionData(), - isToml: Boolean = false, - showUseKts: Boolean = false, - showMinSdk: Boolean = true, - showLanguage: Boolean = true, - crossinline block: ProjectTemplateBuilder.() -> Unit + projectName: StringParameter = projectNameParameter(), + packageName: StringParameter = packageNameParameter(), + useKts: BooleanParameter = useKtsParameter(), + minSdk: EnumParameter = minSdkParameter(), + language: EnumParameter = projectLanguageParameter(), + projectVersionData: ProjectVersionData = ProjectVersionData(), + isToml: Boolean = false, + showUseKts: Boolean = false, + showMinSdk: Boolean = true, + showLanguage: Boolean = true, + crossinline block: ProjectTemplateBuilder.() -> Unit ): ProjectTemplate { - return ProjectTemplateBuilder().apply { - - // When project name is changed, change the package name accordingly - projectName.observe { name -> - val newPackage = - AndroidUtils.appNameToPackageName(name.value, packageName.value) - packageName.setValue(newPackage) - } - - Environment.mkdirIfNotExists(Environment.PROJECTS_DIR) - - val saveLocation = stringParameter { - name = R.string.wizard_save_location - default = Environment.PROJECTS_DIR.absolutePath - endIcon = { R.drawable.ic_folder } - constraints = listOf(NONEMPTY, DIRECTORY, EXISTS) - inputType = - android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE - maxLines = 1 - tooltipTag = "setup.save.location" - } - - projectName.doBeforeCreateView { - it.setValue(getNewProjectName(saveLocation.value, projectName.value)) - } - - widgets( - TextFieldWidget(projectName), TextFieldWidget(packageName), - TextFieldWidget(saveLocation) - ) - - if (showLanguage) { - widgets(SpinnerWidget(language)) - } - - if (showMinSdk) { - widgets(SpinnerWidget(minSdk)) - } - - if (showUseKts) { - widgets(CheckBoxWidget(useKts)) - } - - // Setup the required properties before executing the recipe - preRecipe = { - this@apply._executor = this - - if (!showUseKts) { - useKts.setValue(true, notify = false) - } - - this@apply._data = ProjectTemplateData( - projectName.value, - File(saveLocation.value, projectName.value), - projectVersionData, - language = if (showLanguage) language.value else null, - useKts = useKts.value, - useToml = isToml - ) - - if (data.projectDir.exists() && data.projectDir.listFiles() - ?.isNotEmpty() == true - ) { - throw IllegalArgumentException("Project directory already exists") - } - - setDefaultModuleData( - ModuleTemplateData( - ":app", appName = data.name, packageName.value, - data.moduleNameToDir(":app"), type = AndroidApp, - language = if (showLanguage) language.value else null, - minSdk = if (showMinSdk) minSdk.value else null, - useKts = data.useKts, useToml = isToml + return ProjectTemplateBuilder().apply { + + // When project name is changed, change the package name accordingly + projectName.observe { name -> + val newPackage = AndroidUtils.appNameToPackageName(name.value, packageName.value) + packageName.setValue(newPackage) + } + + Environment.mkdirIfNotExists(Environment.PROJECTS_DIR) + + val saveLocation = stringParameter { + name = R.string.wizard_save_location + default = Environment.PROJECTS_DIR.absolutePath + endIcon = { R.drawable.ic_folder } + constraints = listOf(NONEMPTY, DIRECTORY, EXISTS) + inputType = + android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + imeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE + maxLines = 1 + tooltipTag = "setup.save.location" + } + + projectName.doBeforeCreateView { + it.setValue(getNewProjectName(saveLocation.value, projectName.value)) + } + + widgets( + TextFieldWidget(projectName), TextFieldWidget(packageName), + TextFieldWidget(saveLocation) ) - ) - } - block() + if (showLanguage) { + widgets(SpinnerWidget(language)) + } + + if (showMinSdk) { + widgets(SpinnerWidget(minSdk)) + } + + if (showUseKts) { + widgets(CheckBoxWidget(useKts)) + } + + // Setup the required properties before executing the recipe + preRecipe = { + this@apply._executor = this + + if (!showUseKts) { + useKts.setValue(true, notify = false) + } + + this@apply._data = ProjectTemplateData( + projectName.value, + File(saveLocation.value, projectName.value), + projectVersionData, + language = if (showLanguage) language.value else null, + useKts = useKts.value, + useToml = isToml + ) + + if (data.projectDir.exists() && data.projectDir.listFiles() + ?.isNotEmpty() == true + ) { + throw IllegalArgumentException("Project directory already exists") + } - }.build() as ProjectTemplate + setDefaultModuleData( + ModuleTemplateData( + ":app", appName = data.name, packageName.value, + data.moduleNameToDir(":app"), type = AndroidApp, + language = if (showLanguage) language.value else null, + minSdk = if (showMinSdk) minSdk.value else null, + useKts = data.useKts, useToml = isToml + ) + ) + } + + block() + + }.build() as ProjectTemplate } diff --git a/templates-impl/build.gradle.kts b/templates-impl/build.gradle.kts index e9aa88ea52..db9b091c47 100644 --- a/templates-impl/build.gradle.kts +++ b/templates-impl/build.gradle.kts @@ -41,9 +41,9 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.google.auto.service.annotations) - implementation(libs.pebble) + implementation(libs.pebble) - testImplementation(projects.templatesApi) + testImplementation(projects.templatesApi) testImplementation(projects.lsp.api) testImplementation(projects.preferences) testImplementation(projects.testing.unit) 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 f458479688..e3edf59a25 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 @@ -39,57 +39,55 @@ import java.util.zip.ZipFile @AutoService(ITemplateProvider::class) class TemplateProviderImpl : ITemplateProvider { - companion object { - private val log = LoggerFactory.getLogger(TemplateProviderImpl::class.java) - } - - private val templates = mutableMapOf>() - - init { - reload() - } - - private fun initializeTemplates() { - val folder = TEMPLATES_DIR - log.debug("Template listing archives in: ${folder.toString()} with extension: ${TEMPLATE_ARCHIVE_EXTENSION}") - val list = folder.listFiles { file -> file.extension == TEMPLATE_ARCHIVE_EXTENSION } ?: return - - for (zipFile in list) { - log.debug("Template archive: $zipFile") - try { - val zipTemplates = ZipTemplateReader.read(zipFile) { json, params, path, data, defModule -> - ZipRecipeExecutor({ ZipFile(zipFile) }, json, params, path, data, defModule) - } + companion object { + private val log = LoggerFactory.getLogger(TemplateProviderImpl::class.java) + } + + private val templates = mutableMapOf>() - for (t in zipTemplates) { - log.debug("template: $t") - templates[t.templateId] = t + init { + reload() + } + + private fun initializeTemplates() { + val folder = TEMPLATES_DIR + log.debug("Template listing archives in: ${folder.toString()} with extension: ${TEMPLATE_ARCHIVE_EXTENSION}") + val list = folder.listFiles { file -> file.extension == TEMPLATE_ARCHIVE_EXTENSION } ?: return + + for (zipFile in list) { + log.debug("Template archive: $zipFile") + try { + val zipTemplates = ZipTemplateReader.read(zipFile) { json, params, path, data, defModule -> + ZipRecipeExecutor({ ZipFile(zipFile) }, json, params, path, data, defModule) + } + + for (t in zipTemplates) { + log.debug("template: $t") + templates[t.templateId] = t + } + + log.debug("templates: $templates") + } catch (e: Exception) { + log.error("Failed to load template from archive: $zipFile", e) + } } + } + + override fun getTemplates(): List> { + return ImmutableList.copyOf(templates.values) + } + + override fun getTemplate(templateId: String): Template<*>? { + return templates[templateId] + } + + override fun reload() { + release() + initializeTemplates() + } - log.debug("templates: $templates") - } catch (e: Exception) { - log.error("Failed to load template from archive: $zipFile", e) - } + override fun release() { + templates.forEach { it.value.release() } + templates.clear() } - } - - override fun getTemplates(): List> { - return ImmutableList.copyOf(templates.values) - } - - override fun getTemplate(templateId: String): Template<*>? { - return templates[templateId] - } - - override fun reload() { - release() - // CoroutineScope(Dispatchers.IO).launch { - initializeTemplates() - // } - } - - override fun release() { - templates.forEach { it.value.release() } - templates.clear() - } } \ No newline at end of file diff --git a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt index 806e87db5e..f7b1bd25a5 100644 --- a/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt +++ b/templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipJson.kt @@ -5,60 +5,60 @@ data class TemplatesIndex(val templates: List) data class TemplateRef(val path: String, val experimental: Boolean = false,) data class TemplateJson( - val name: String, - val description: String?, - val version: String?, - val tooltipTag: String = "", - val parameters: ParametersJson? = null, - val system: SystemParametersJson? = null + val name: String, + val description: String?, + val version: String?, + val tooltipTag: String = "", + val parameters: ParametersJson? = null, + val system: SystemParametersJson? = null ) data class ParametersJson( - val required: RequiredParametersJson? = null, - val optional: OptionalParametersJson? = null, - val user: UserParametersJson? = null + val required: RequiredParametersJson? = null, + val optional: OptionalParametersJson? = null, + val user: UserParametersJson? = null ) data class RequiredParametersJson( - val appName: IdentifierJson, - val packageName: IdentifierJson, - val saveLocation: IdentifierJson + val appName: IdentifierJson, + val packageName: IdentifierJson, + val saveLocation: IdentifierJson ) data class OptionalParametersJson( - val language: IdentifierJson? = null, - val minsdk: IdentifierJson? = null + val language: IdentifierJson? = null, + val minsdk: IdentifierJson? = null ) data class SystemParametersJson( - val agpVersion: IdentifierJson, - val kotlinVersion: IdentifierJson, - val gradleVersion: IdentifierJson, - val compileSdk: IdentifierJson, - val targetSdk: IdentifierJson, - val javaSourceCompat: IdentifierJson, - val javaTargetCompat: IdentifierJson, - val javaTarget: IdentifierJson + val agpVersion: IdentifierJson, + val kotlinVersion: IdentifierJson, + val gradleVersion: IdentifierJson, + val compileSdk: IdentifierJson, + val targetSdk: IdentifierJson, + val javaSourceCompat: IdentifierJson, + val javaTargetCompat: IdentifierJson, + val javaTarget: IdentifierJson ) data class IdentifierJson( - val identifier: String + val identifier: String ) data class UserParametersJson( - val text: List = emptyList(), - val checkbox: List = emptyList() + val text: List = emptyList(), + val checkbox: List = emptyList() ) data class TextParameterJson( - val label: String, - val identifier: String, - val default: String? = null + val label: String, + val identifier: String, + val default: String? = null ) data class CheckboxParameterJson( - val label: String, - val identifier: String, - val default: Boolean? = null + val label: String, + val identifier: String, + val default: Boolean? = null ) 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 3f00cdaa08..a7975e7f81 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 @@ -24,246 +24,246 @@ import org.adfa.constants.KOTLIN_VERSION import org.adfa.constants.Sdk class ZipRecipeExecutor( - private val zipProvider: () -> ZipFile, - private val metaJson: TemplateJson, - private val params: MutableMap>, - private val basePath: String, - private val data: ProjectTemplateData, - private val defModule: ModuleTemplateData, + private val zipProvider: () -> ZipFile, + private val metaJson: TemplateJson, + private val params: MutableMap>, + private val basePath: String, + private val data: ProjectTemplateData, + private val defModule: ModuleTemplateData, ) : TemplateRecipe { - companion object { - private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java) - } + companion object { + private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java) + } - override fun execute( + override fun execute( executor: RecipeExecutor - ): ProjectTemplateRecipeResult { + ): ProjectTemplateRecipeResult { - log.debug("executor called!!") + log.debug("executor called!!") - val projectDir = data.projectDir - if (projectDir.exists()) { - return ProjectTemplateRecipeResultImpl(data) - } + val projectDir = data.projectDir + if (projectDir.exists()) { + return ProjectTemplateRecipeResultImpl(data) + } - val projectRoot = projectDir.canonicalFile + val projectRoot = projectDir.canonicalFile - val flags: Map = - params.mapNotNull { (identifier, param) -> - (param.value as? Boolean)?.let { identifier to it } - }.toMap() + val flags: Map = + params.mapNotNull { (identifier, param) -> + (param.value as? Boolean)?.let { identifier to it } + }.toMap() - zipProvider().use { zip -> + 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 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 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 (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) + 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 + 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 + 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 normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue - val relativePath = normalized.removePrefix("$basePath/") - .replace(packageName.value, defModule.packageName.replace(".", "/")) + val relativePath = normalized.removePrefix("$basePath/") + .replace(packageName.value, defModule.packageName.replace(".", "/")) - val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile + 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 - } + if (!outFile.toPath().startsWith(projectRoot.toPath())) { + log.warn("Skipping suspicious ZIP entry outside project dir: {}", entry.name) + continue + } - 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) - } + 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) + } + } + } + } } - } } - } - } - keystore(executor) + keystore(executor) - return ProjectTemplateRecipeResultImpl(data) - } - - private fun keystore(executor: RecipeExecutor) { - val storeSrc = Environment.KEYSTORE_RELEASE - val storeDest = File(data.projectDir, Environment.KEYSTORE_RELEASE_NAME) - if (storeSrc.exists()) { - executor.copy(storeSrc, storeDest) + return ProjectTemplateRecipeResultImpl(data) } + private fun keystore(executor: RecipeExecutor) { + val storeSrc = Environment.KEYSTORE_RELEASE + val storeDest = File(data.projectDir, Environment.KEYSTORE_RELEASE_NAME) + if (storeSrc.exists()) { + executor.copy(storeSrc, storeDest) + } + - val propsSrc = Environment.KEYSTORE_PROPERTIES - val propsDest = File(data.projectDir, Environment.KEYSTORE_PROPERTIES_NAME) - if (propsSrc.exists()) { - executor.copy(propsSrc, propsDest) + val propsSrc = Environment.KEYSTORE_PROPERTIES + val propsDest = File(data.projectDir, Environment.KEYSTORE_PROPERTIES_NAME) + if (propsSrc.exists()) { + executor.copy(propsSrc, propsDest) + } } - } - - private fun shouldSkipFile(name: String, language: String): Boolean { - // If language is Kotlin, skip .java files - // If language is Java, skip .kt files - val ext = name.substringAfterLast('.', "").lowercase() - return when (language.lowercase()) { - LANGUAGE_KOTLIN -> ext == FILE_EXT_JAVA - LANGUAGE_JAVA -> ext == FILE_EXT_KOTLIN - else -> false + + private fun shouldSkipFile(name: String, language: String): Boolean { + // If language is Kotlin, skip .java files + // If language is Java, skip .kt files + val ext = name.substringAfterLast('.', "").lowercase() + return when (language.lowercase()) { + LANGUAGE_KOTLIN -> ext == FILE_EXT_JAVA + LANGUAGE_JAVA -> ext == FILE_EXT_KOTLIN + else -> false + } } - } - private fun safeLanguageName(language: Language?): String = - language?.name?.lowercase() ?: "" + private fun safeLanguageName(language: Language?): String = + language?.name?.lowercase() ?: "" - private fun safeMinSdkApi(minSdk: Sdk?): String = - minSdk?.api?.toString() ?: "" + private fun safeMinSdkApi(minSdk: Sdk?): String = + minSdk?.api?.toString() ?: "" - private fun TemplateJson.pebbleParams( - data: ProjectTemplateData, - defModule: ModuleTemplateData, - params: MutableMap> - ): Pair, List> { + private fun TemplateJson.pebbleParams( + data: ProjectTemplateData, + defModule: ModuleTemplateData, + params: MutableMap> + ): Pair, List> { - val warnings = mutableListOf() + val warnings = mutableListOf() - val appName = resolveString(parameters?.required?.appName?.identifier, KEY_APP_NAME) - if (appName.usedDefault) warnings += "Missing 'appName', defaulted to $KEY_APP_NAME" + val appName = resolveString(parameters?.required?.appName?.identifier, KEY_APP_NAME) + if (appName.usedDefault) warnings += "Missing 'appName', defaulted to $KEY_APP_NAME" - val packageName = resolveString(parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) - if (packageName.usedDefault) warnings += "Missing 'packageName', defaulted to $KEY_PACKAGE_NAME" + val packageName = resolveString(parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME) + if (packageName.usedDefault) warnings += "Missing 'packageName', defaulted to $KEY_PACKAGE_NAME" - val saveLocation = resolveString(parameters?.required?.saveLocation?.identifier, KEY_SAVE_LOCATION) - if (saveLocation.usedDefault) warnings += "Missing 'saveLocation', defaulted to $KEY_SAVE_LOCATION" + val saveLocation = resolveString(parameters?.required?.saveLocation?.identifier, KEY_SAVE_LOCATION) + if (saveLocation.usedDefault) warnings += "Missing 'saveLocation', defaulted to $KEY_SAVE_LOCATION" - val language = resolveString(parameters?.optional?.language?.identifier, KEY_LANGUAGE) - if (language.usedDefault) warnings += "Missing 'language', defaulted to $KEY_LANGUAGE" + val language = resolveString(parameters?.optional?.language?.identifier, KEY_LANGUAGE) + if (language.usedDefault) warnings += "Missing 'language', defaulted to $KEY_LANGUAGE" - val minSdk = resolveString(parameters?.optional?.minsdk?.identifier, KEY_MIN_SDK) - if (minSdk.usedDefault) warnings += "Missing 'minsdk', defaulted to $KEY_MIN_SDK" + val minSdk = resolveString(parameters?.optional?.minsdk?.identifier, KEY_MIN_SDK) + if (minSdk.usedDefault) warnings += "Missing 'minsdk', defaulted to $KEY_MIN_SDK" - val agpVersion = resolveString(system?.agpVersion?.identifier, KEY_AGP_VERSION) - if (agpVersion.usedDefault) warnings += "Missing 'agpVersion', defaulted to $KEY_AGP_VERSION" + val agpVersion = resolveString(system?.agpVersion?.identifier, KEY_AGP_VERSION) + if (agpVersion.usedDefault) warnings += "Missing 'agpVersion', defaulted to $KEY_AGP_VERSION" - val kotlinVersion = resolveString(system?.kotlinVersion?.identifier, KEY_KOTLIN_VERSION) - if (kotlinVersion.usedDefault) warnings += "Missing 'kotlinVersion', defaulted to $KEY_KOTLIN_VERSION" + val kotlinVersion = resolveString(system?.kotlinVersion?.identifier, KEY_KOTLIN_VERSION) + if (kotlinVersion.usedDefault) warnings += "Missing 'kotlinVersion', defaulted to $KEY_KOTLIN_VERSION" - val gradleVersion = resolveString(system?.gradleVersion?.identifier, KEY_GRADLE_VERSION) - if (gradleVersion.usedDefault) warnings += "Missing 'gradleVersion', defaulted to $KEY_GRADLE_VERSION" + val gradleVersion = resolveString(system?.gradleVersion?.identifier, KEY_GRADLE_VERSION) + if (gradleVersion.usedDefault) warnings += "Missing 'gradleVersion', defaulted to $KEY_GRADLE_VERSION" - val compileSdk = resolveString(system?.compileSdk?.identifier, KEY_COMPILE_SDK) - if (compileSdk.usedDefault) warnings += "Missing 'compileSdk', defaulted to $KEY_COMPILE_SDK" + val compileSdk = resolveString(system?.compileSdk?.identifier, KEY_COMPILE_SDK) + if (compileSdk.usedDefault) warnings += "Missing 'compileSdk', defaulted to $KEY_COMPILE_SDK" - val targetSdk = resolveString(system?.targetSdk?.identifier, KEY_TARGET_SDK) - if (targetSdk.usedDefault) warnings += "Missing 'targetSdk', defaulted to $KEY_TARGET_SDK" + val targetSdk = resolveString(system?.targetSdk?.identifier, KEY_TARGET_SDK) + if (targetSdk.usedDefault) warnings += "Missing 'targetSdk', defaulted to $KEY_TARGET_SDK" - val javaSourceCompat = resolveString(system?.javaSourceCompat?.identifier, KEY_JAVA_SOURCE_COMPAT) - if (javaSourceCompat.usedDefault) warnings += "Missing 'javaSourceCompat', defaulted to $KEY_JAVA_SOURCE_COMPAT" + val javaSourceCompat = resolveString(system?.javaSourceCompat?.identifier, KEY_JAVA_SOURCE_COMPAT) + if (javaSourceCompat.usedDefault) warnings += "Missing 'javaSourceCompat', defaulted to $KEY_JAVA_SOURCE_COMPAT" - val javaTargetCompat = resolveString(system?.javaTargetCompat?.identifier, KEY_JAVA_TARGET_COMPAT) - if (javaTargetCompat.usedDefault) warnings += "Missing 'javaTargetCompat', defaulted to $KEY_JAVA_TARGET_COMPAT" + val javaTargetCompat = resolveString(system?.javaTargetCompat?.identifier, KEY_JAVA_TARGET_COMPAT) + if (javaTargetCompat.usedDefault) warnings += "Missing 'javaTargetCompat', defaulted to $KEY_JAVA_TARGET_COMPAT" - val javaTarget = resolveString(system?.javaTarget?.identifier, KEY_JAVA_TARGET) - if (javaTarget.usedDefault) warnings += "Missing 'javaTarget', defaulted to $KEY_JAVA_TARGET" + val javaTarget = resolveString(system?.javaTarget?.identifier, KEY_JAVA_TARGET) + if (javaTarget.usedDefault) warnings += "Missing 'javaTarget', defaulted to $KEY_JAVA_TARGET" - val baseMap = mapOf( - appName.value to data.name, - packageName.value to defModule.packageName, - saveLocation.value to data.projectDir.toString(), - language.value to safeLanguageName(data.language), - minSdk.value to safeMinSdkApi(defModule.versions.minSdk), - agpVersion.value to ANDROID_GRADLE_PLUGIN_VERSION, - kotlinVersion.value to KOTLIN_VERSION, - gradleVersion.value to data.version.gradle, - compileSdk.value to defModule.versions.compileSdk.api.toString(), - targetSdk.value to defModule.versions.targetSdk.api.toString(), - javaSourceCompat.value to defModule.versions.javaSource(), - javaTargetCompat.value to defModule.versions.javaTarget(), - javaTarget.value to defModule.versions.javaTarget + val baseMap = mapOf( + appName.value to data.name, + packageName.value to defModule.packageName, + saveLocation.value to data.projectDir.toString(), + language.value to safeLanguageName(data.language), + minSdk.value to safeMinSdkApi(defModule.versions.minSdk), + agpVersion.value to ANDROID_GRADLE_PLUGIN_VERSION, + kotlinVersion.value to KOTLIN_VERSION, + gradleVersion.value to data.version.gradle, + compileSdk.value to defModule.versions.compileSdk.api.toString(), + targetSdk.value to defModule.versions.targetSdk.api.toString(), + javaSourceCompat.value to defModule.versions.javaSource(), + javaTargetCompat.value to defModule.versions.javaTarget(), + javaTarget.value to defModule.versions.javaTarget + ) + + val map = baseMap + params.mapValues { (_, param) -> + param.value ?: "" + } + + return map to warnings + } + + data class ResolvedParam( + val value: T, + val usedDefault: Boolean ) - val map = baseMap + params.mapValues { (_, param) -> - param.value ?: "" + private fun resolveString(value: String?, default: String): ResolvedParam { + return if (value.isNullOrBlank()) ResolvedParam(default, true) + else ResolvedParam(value, false) } - return map to warnings - } - - data class ResolvedParam( - val value: T, - val usedDefault: Boolean - ) - - private fun resolveString(value: String?, default: String): ResolvedParam { - return if (value.isNullOrBlank()) ResolvedParam(default, true) - else ResolvedParam(value, false) - } - - private fun resolveBoolean(raw: Boolean?, default: Boolean): ResolvedParam { - return if (raw == null) ResolvedParam(default, true) - else ResolvedParam(raw, false) - } - - private fun filterAndNormalizeZipEntry( - entryName: String, - flags: Map - ): String? { - val parts = entryName.split(File.separator).filter { it.isNotEmpty() } - if (parts.isEmpty()) return null - - val normalizedParts = mutableListOf() - - for (part in parts) { - when (flags[part]) { - null -> normalizedParts.add(part) - true -> { } - false -> return null + private fun resolveBoolean(raw: Boolean?, default: Boolean): ResolvedParam { + return if (raw == null) ResolvedParam(default, true) + else ResolvedParam(raw, false) } - } - return normalizedParts.joinToString(File.separator) - } + private fun filterAndNormalizeZipEntry( + entryName: String, + flags: Map + ): String? { + val parts = entryName.split(File.separator).filter { it.isNotEmpty() } + if (parts.isEmpty()) return null + + val normalizedParts = mutableListOf() + + for (part in parts) { + when (flags[part]) { + null -> normalizedParts.add(part) + true -> { } + false -> return null + } + } + + return normalizedParts.joinToString(File.separator) + } } 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 da0d26cfc7..8cfab63bdb 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 @@ -19,109 +19,105 @@ import java.io.File import java.util.zip.ZipFile object ZipTemplateReader { - private val log = LoggerFactory.getLogger(ZipTemplateReader::class.java) - - private val gson = Gson() - - fun read( - zipFile: File, - recipeFactory: (TemplateJson, MutableMap>, String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe - ): List { - - val templates = mutableListOf() - - try { - ZipFile(zipFile).use { zip -> - - log.debug("zipFile: $zipFile") - - val indexEntry = zip.getEntry(ARCHIVE_JSON) ?: return emptyList() - val indexJson = zip.getInputStream(indexEntry).bufferedReader().use { - gson.fromJson(it, TemplatesIndex::class.java) - } - - log.debug("indexJson: $indexJson") - for (templateRef in indexJson.templates) { - try { - val basePath = templateRef.path - log.debug("basePath: $basePath") - val metaEntry = zip.getEntry("$basePath/$META_FOLDER/$META_JSON") ?: continue - - val metaJsonString = zip.getInputStream(metaEntry).bufferedReader().use { reader -> - reader.readText() + private val log = LoggerFactory.getLogger(ZipTemplateReader::class.java) + + private val gson = Gson() + + fun read( + zipFile: File, + recipeFactory: (TemplateJson, MutableMap>, String, ProjectTemplateData, ModuleTemplateData) -> TemplateRecipe + ): List { + + val templates = mutableListOf() + + try { + ZipFile(zipFile).use { zip -> + + log.debug("zipFile: $zipFile") + + val indexEntry = zip.getEntry(ARCHIVE_JSON) ?: return emptyList() + val indexJson = zip.getInputStream(indexEntry).bufferedReader().use { + gson.fromJson(it, TemplatesIndex::class.java) + } + + log.debug("indexJson: $indexJson") + for (templateRef in indexJson.templates) { + try { + val basePath = templateRef.path + log.debug("basePath: $basePath") + val metaEntry = zip.getEntry("$basePath/$META_FOLDER/$META_JSON") ?: continue + + val metaJsonString = zip.getInputStream(metaEntry).bufferedReader().use { reader -> + reader.readText() + } + + val metaJson = gson.fromJson(metaJsonString, TemplateJson::class.java) + + log.debug("metaJson: $metaJson") + + val thumbEntry = zip.getEntry("$basePath/$META_FOLDER/$META_THUMBNAIL") + val thumbData = thumbEntry?.let { zip.getInputStream(it).use { s -> s.readBytes() } } + + if (thumbData == null) log.error("template $basePath/$META_FOLDER/$META_THUMBNAIL not found or is invalid") + log.debug("thumbData: $thumbData") + + val userWidgets = mutableListOf>() + val params = mutableMapOf>() + + metaJson.parameters?.user?.text?.forEach { textParam -> + val param = stringParameter { + name = 0 + nameStr = textParam.label ?: "" + default = textParam.default ?: "" + } + userWidgets.add(TextFieldWidget(param)) + params[textParam.identifier] = param + } + + metaJson.parameters?.user?.checkbox?.forEach { checkboxParam -> + val param = booleanParameter { + name = 0 + nameStr = checkboxParam.label ?: "" + default = checkboxParam.default ?: false + } + userWidgets.add(CheckBoxWidget(param)) + params[checkboxParam.identifier] = param + } + + val project = baseZipProject( + showLanguage = (metaJson.parameters?.optional?.language != null), + showMinSdk = (metaJson.parameters?.optional?.minsdk != null) + ) { + + this.templateNameStr = metaJson.name + this.tooltipTag = metaJson.tooltipTag + this.thumbData = thumbData + + this.templateName = 0 + this.thumb = R.drawable.template_no_activity + + for (widget in userWidgets) { + widgets(widget) + } + + log.debug("this.name: ${this.templateNameStr}") + this.recipe = TemplateRecipe { executor -> + val innerRecipe = recipeFactory(metaJson, params, basePath, data, defModule) + innerRecipe.execute(executor) + } + } + + log.debug("adding project ${metaJson.name}") + templates.add(project) + } catch (e: Exception) { + log.error("Failed to load template at ${templateRef.path}", e) + } + } } - - val metaJson = gson.fromJson(metaJsonString, TemplateJson::class.java) - - log.debug("metaJson: $metaJson") - - val thumbEntry = zip.getEntry("$basePath/$META_FOLDER/$META_THUMBNAIL") - val thumbData = thumbEntry?.let { zip.getInputStream(it).use { s -> s.readBytes() } } - - if (thumbData == null) log.error("template $basePath/$META_FOLDER/$META_THUMBNAIL not found or is invalid") - log.debug("thumbData: $thumbData") - - val userWidgets = mutableListOf>() - val params = mutableMapOf>() - - metaJson.parameters?.user?.text?.forEach { textParam -> - val param = stringParameter { - // name = string.project_app_name - name = 0 - nameStr = textParam.label ?: "" - default = textParam.default ?: "" - - } - userWidgets.add(TextFieldWidget(param)) - params[textParam.identifier] = param - } - - metaJson.parameters?.user?.checkbox?.forEach { checkboxParam -> - val param = booleanParameter { - // name = string.project_app_name - name = 0 - nameStr = checkboxParam.label ?: "" - default = checkboxParam.default ?: false - } - userWidgets.add(CheckBoxWidget(param)) - params[checkboxParam.identifier] = param - } - - val project = baseZipProject( - showLanguage = (metaJson.parameters?.optional?.language != null), - showMinSdk = (metaJson.parameters?.optional?.minsdk != null) - ) { - - this.templateNameStr = metaJson.name - this.tooltipTag = metaJson.tooltipTag - this.thumbData = thumbData - - this.templateName = 0 - this.thumb = R.drawable.template_no_activity - - for (widget in userWidgets) { - widgets(widget) - } - - log.debug("this.name: ${this.templateNameStr}") - this.recipe = TemplateRecipe { executor -> - val innerRecipe = recipeFactory(metaJson, params, basePath, data, defModule) - innerRecipe.execute(executor) - } - } - - log.debug("adding project ${metaJson.name}") - templates.add(project) - } catch (e: Exception) { - log.error("Failed to load template at ${templateRef.path}", e) - } + } catch (e: Exception) { + log.error("Failed to read zip file $zipFile", e) } - } - } catch (e: Exception) { - log.error("Failed to read zip file $zipFile", e) - } - - return templates - } + return templates + } } From 69695e4b2cfe42b47285be17af342d88cb651bd2 Mon Sep 17 00:00:00 2001 From: Joel Menchavez Date: Fri, 20 Mar 2026 23:04:19 +0800 Subject: [PATCH 25/25] formatting --- .../java/com/itsaky/androidide/adapters/TemplateListAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt index 2b63f48755..f6e95acd19 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/TemplateListAdapter.kt @@ -66,7 +66,7 @@ class TemplateListAdapter( holder.binding.apply { val template = templates[position] - if (template == Template.EMPTY) { + if (template == Template.EMPTY) { root.visibility = View.INVISIBLE return@apply }