Skip to content

Commit 4a3dfd9

Browse files
authored
Merge branch 'stage' into feat/ADFA-3125-filetree-drag-and-drop
2 parents 760662e + ec0a78f commit 4a3dfd9

File tree

10 files changed

+161
-76
lines changed

10 files changed

+161
-76
lines changed

app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ class MainActivity : EdgeToEdgeIDEActivity() {
400400
builder.show()
401401
}
402402

403-
internal fun openProject(root: File, project: RecentProject? = null) {
403+
internal fun openProject(root: File, project: RecentProject? = null, hasTemplateIssues: Boolean = false) {
404404
ProjectManagerImpl.getInstance().projectPath = root.absolutePath
405405
GeneralPreferences.lastOpenedProject = root.absolutePath
406406

@@ -416,7 +416,7 @@ class MainActivity : EdgeToEdgeIDEActivity() {
416416
}
417417

418418
// Track project open in Firebase Analytics
419-
analyticsManager.trackProjectOpened(root.absolutePath)
419+
analyticsManager.trackProjectOpened(root.absolutePath)
420420

421421
if (isFinishing) {
422422
return
@@ -425,6 +425,9 @@ class MainActivity : EdgeToEdgeIDEActivity() {
425425
val intent =
426426
Intent(this, EditorActivityKt::class.java).apply {
427427
putExtra("PROJECT_PATH", root.absolutePath)
428+
if (hasTemplateIssues) {
429+
putExtra("HAS_TEMPLATE_ISSUES", true)
430+
}
428431
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
429432
}
430433

app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
214214

215215
observeStates()
216216
startServices()
217+
218+
if (intent.getBooleanExtra("HAS_TEMPLATE_ISSUES", false)) {
219+
flashError(getString(string.msg_template_warnings))
220+
}
221+
217222
}
218223

219224
private fun observeStates() {

app/src/main/java/com/itsaky/androidide/fragments/TemplateDetailsFragment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ class TemplateDetailsFragment :
138138
// open the project
139139
(requireActivity() as MainActivity).openProject(
140140
result.data.projectDir,
141-
project = project
141+
project = project,
142+
hasTemplateIssues = result.hasErrorsWarnings
142143
)
143144
}
144145
}

app/src/main/java/com/itsaky/androidide/fragments/TemplateListFragment.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import com.itsaky.androidide.idetooltips.TooltipManager
2929
import com.itsaky.androidide.idetooltips.TooltipTag.EXIT_TO_MAIN
3030
import com.itsaky.androidide.templates.ITemplateProvider
3131
import com.itsaky.androidide.templates.ProjectTemplate
32+
import com.itsaky.androidide.templates.impl.TemplateProviderImpl
33+
import com.itsaky.androidide.utils.flashError
3234
import com.itsaky.androidide.viewmodel.MainViewModel
3335
import org.slf4j.LoggerFactory
3436

@@ -115,11 +117,9 @@ class TemplateListFragment :
115117

116118
log.debug("Reloading templates...")
117119

118-
val templates =
119-
ITemplateProvider
120-
.getInstance(reload = true)
121-
.getTemplates()
122-
.filterIsInstance<ProjectTemplate>()
120+
val provider = ITemplateProvider.getInstance(reload = true)
121+
val templates = provider.getTemplates().filterIsInstance<ProjectTemplate>()
122+
val warnings = (provider as? TemplateProviderImpl)?.warnings.orEmpty()
123123

124124
adapter =
125125
TemplateListAdapter(
@@ -140,5 +140,9 @@ class TemplateListFragment :
140140
)
141141
binding.list.adapter = adapter
142142
updateSpanCount()
143-
}
143+
144+
if (warnings.isNotEmpty()) {
145+
requireActivity().flashError(warnings.joinToString("\n"))
146+
}
147+
}
144148
}

resources/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,8 @@
10291029
<string name="checkbox_delete_source_after_install">Delete installation file after install</string>
10301030
<string name="init_failed_with_reason">%1$s: %2$s</string>
10311031

1032+
<string name="msg_template_warnings">\n\nProject creation finished with warnings/errors. Open IDE Logs for details.</string>
1033+
10321034
<!-- Name of the notification channel which is used to show notifications for wireless debugging -->
10331035
<string name="notification_channel_adb_pairing">Wireless debugging pairing</string>
10341036

templates-api/src/main/java/com/itsaky/androidide/templates/template.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ val data: D
5858
/**
5959
* Result of recipe execution for a [ProjectTemplate].
6060
*/
61-
interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData<ProjectTemplateData>
61+
interface ProjectTemplateRecipeResult : TemplateRecipeResultWithData<ProjectTemplateData> {
62+
val hasErrorsWarnings: Boolean
63+
get() = false
64+
}
6265

6366
/**
6467
* Result of recipe execution for a [ModuleTemplate].

templates-impl/src/main/java/com/itsaky/androidide/templates/impl/TemplateProviderImpl.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class TemplateProviderImpl : ITemplateProvider {
4444
}
4545

4646
private val templates = mutableMapOf<String, Template<*>>()
47+
val warnings: MutableList<String> = mutableListOf()
4748

4849
init {
4950
reload()
@@ -55,14 +56,15 @@ class TemplateProviderImpl : ITemplateProvider {
5556

5657
for (zipFile in list) {
5758
try {
58-
val zipTemplates = ZipTemplateReader.read(zipFile) { json, params, path, data, defModule ->
59+
val zipTemplates = ZipTemplateReader.read(zipFile, warnings) { json, params, path, data, defModule ->
5960
ZipRecipeExecutor({ ZipFile(zipFile) }, json, params, path, data, defModule)
6061
}
6162

6263
for (t in zipTemplates) {
6364
templates[t.templateId] = t
6465
}
6566
} catch (e: Exception) {
67+
warnings.add("Failed to load template archive $zipFile error: ${e.message}")
6668
log.error("Failed to load template from archive: $zipFile", e)
6769
}
6870
}
@@ -78,6 +80,7 @@ class TemplateProviderImpl : ITemplateProvider {
7880

7981
override fun reload() {
8082
release()
83+
warnings.clear()
8184
initializeTemplates()
8285
}
8386

templates-impl/src/main/java/com/itsaky/androidide/templates/impl/base/results.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@ import com.itsaky.androidide.templates.base.ModuleTemplateBuilder
2525
import com.itsaky.androidide.templates.base.ProjectTemplateBuilder
2626

2727
data class ProjectTemplateRecipeResultImpl(
28-
override val data: ProjectTemplateData
28+
override val data: ProjectTemplateData,
29+
override val hasErrorsWarnings: Boolean = false
2930
) : ProjectTemplateRecipeResult
3031

3132
data class ModuleTemplateRecipeResultImpl(override val data: ModuleTemplateData
3233
) : ModuleTemplateRecipeResult
3334

3435

3536
internal fun ProjectTemplateBuilder.recipeResult(): ProjectTemplateRecipeResult {
36-
return ProjectTemplateRecipeResultImpl(data)
37+
return ProjectTemplateRecipeResultImpl(data)
3738
}
3839

3940
internal fun ModuleTemplateBuilder.recipeResult(): ModuleTemplateRecipeResult {
40-
return ModuleTemplateRecipeResultImpl(data)
41+
return ModuleTemplateRecipeResultImpl(data)
4142
}

templates-impl/src/main/java/com/itsaky/androidide/templates/impl/zip/ZipRecipeExecutor.kt

Lines changed: 122 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.itsaky.androidide.templates.RecipeExecutor
1818
import com.itsaky.androidide.templates.TemplateRecipe
1919
import com.itsaky.androidide.templates.impl.base.ProjectTemplateRecipeResultImpl
2020
import com.itsaky.androidide.utils.Environment
21+
import io.pebbletemplates.pebble.error.PebbleException
2122

2223
import org.adfa.constants.ANDROID_GRADLE_PLUGIN_VERSION
2324
import org.adfa.constants.KOTLIN_VERSION
@@ -32,6 +33,8 @@ class ZipRecipeExecutor(
3233
private val defModule: ModuleTemplateData,
3334
) : TemplateRecipe<ProjectTemplateRecipeResult> {
3435

36+
var hasErrorsWarnings: Boolean = false
37+
3538
companion object {
3639
private val log = LoggerFactory.getLogger(ZipRecipeExecutor::class.java)
3740
}
@@ -40,11 +43,11 @@ class ZipRecipeExecutor(
4043
executor: RecipeExecutor
4144
): ProjectTemplateRecipeResult {
4245

43-
log.debug("executor called!!")
46+
info("Starting project creation for $basePath")
4447

4548
val projectDir = data.projectDir
4649
if (projectDir.exists()) {
47-
return ProjectTemplateRecipeResultImpl(data)
50+
return ProjectTemplateRecipeResultImpl(data, hasErrorsWarnings)
4851
}
4952

5053
val projectRoot = projectDir.canonicalFile
@@ -56,77 +59,118 @@ class ZipRecipeExecutor(
5659

5760
zipProvider().use { zip ->
5861

59-
val customSyntax = Syntax.Builder()
60-
.setPrintOpenDelimiter(DELIM_PRINT_OPEN)
61-
.setPrintCloseDelimiter(DELIM_PRINT_CLOSE)
62-
.setExecuteOpenDelimiter(DELIM_EXECUTE_OPEN)
63-
.setExecuteCloseDelimiter(DELIM_EXECUTE_CLOSE)
64-
.setCommentOpenDelimiter(DELIM_COMMENT_OPEN)
65-
.setCommentCloseDelimiter(DELIM_COMMENT_CLOSE)
66-
.build()
67-
68-
val pebbleEngine = PebbleEngine.Builder()
69-
.loader(StringLoader())
70-
.syntax(customSyntax)
71-
.build()
72-
73-
val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params)
74-
log.debug("identifiers warnings: ${warnings.joinToString(System.lineSeparator())}")
75-
76-
val packageName =
77-
resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME)
78-
79-
for (entry in zip.entries()) {
80-
if (!entry.name.startsWith("$basePath/")) continue
81-
if (entry.name == "$basePath/") continue
82-
if (entry.name.startsWith("$basePath/$META_FOLDER/")) continue
83-
84-
if ((metaJson.parameters?.optional?.language != null) &&
85-
(data.language != null) &&
86-
shouldSkipFile(
87-
entry.name.removeSuffix(TEMPLATE_EXTENSION),
88-
safeLanguageName(data.language)
89-
)
90-
) continue
91-
92-
val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue
93-
94-
val relativePath = normalized.removePrefix("$basePath/")
95-
.replace(packageName.value, defModule.packageName.replace(".", "/"))
96-
97-
val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile
98-
99-
if (!outFile.toPath().startsWith(projectRoot.toPath())) {
100-
log.warn("Skipping suspicious ZIP entry outside project dir: {}", entry.name)
101-
continue
62+
val customSyntax = Syntax.Builder()
63+
.setPrintOpenDelimiter(DELIM_PRINT_OPEN)
64+
.setPrintCloseDelimiter(DELIM_PRINT_CLOSE)
65+
.setExecuteOpenDelimiter(DELIM_EXECUTE_OPEN)
66+
.setExecuteCloseDelimiter(DELIM_EXECUTE_CLOSE)
67+
.setCommentOpenDelimiter(DELIM_COMMENT_OPEN)
68+
.setCommentCloseDelimiter(DELIM_COMMENT_CLOSE)
69+
.build()
70+
71+
val pebbleEngine = PebbleEngine.Builder()
72+
.loader(StringLoader())
73+
.syntax(customSyntax)
74+
.build()
75+
76+
val (identifiers, warnings) = metaJson.pebbleParams(data, defModule, params)
77+
if (warnings.isNotEmpty()) {
78+
warn("Identifier warnings: ${warnings.joinToString(System.lineSeparator())}")
10279
}
10380

104-
if (entry.isDirectory) {
105-
outFile.mkdirs()
106-
} else {
107-
outFile.parentFile?.mkdirs()
108-
109-
if (entry.name.endsWith(TEMPLATE_EXTENSION)) {
110-
log.debug("template processing ${entry.name}")
111-
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
112-
val template = pebbleEngine.getTemplate(content)
113-
val writer = StringWriter()
114-
template.evaluate(writer, identifiers)
115-
outFile.writeText(writer.toString(), Charsets.UTF_8)
116-
} else {
117-
zip.getInputStream(entry).use { input ->
118-
outFile.outputStream().use { output ->
119-
input.copyTo(output)
81+
val packageName =
82+
resolveString(metaJson.parameters?.required?.packageName?.identifier, KEY_PACKAGE_NAME)
83+
84+
for (entry in zip.entries()) {
85+
if (!entry.name.startsWith("$basePath/")) continue
86+
if (entry.name == "$basePath/") continue
87+
if (entry.name.startsWith("$basePath/$META_FOLDER/")) continue
88+
89+
if ((metaJson.parameters?.optional?.language != null) &&
90+
(data.language != null) &&
91+
shouldSkipFile(
92+
entry.name.removeSuffix(TEMPLATE_EXTENSION),
93+
safeLanguageName(data.language)
94+
)
95+
) continue
96+
97+
val normalized = filterAndNormalizeZipEntry(entry.name, flags) ?: continue
98+
99+
val relativePath = normalized.removePrefix("$basePath/")
100+
.replace(packageName.value, defModule.packageName.replace(".", "/"))
101+
102+
val outFile = File(projectDir, relativePath.removeSuffix(TEMPLATE_EXTENSION)).canonicalFile
103+
104+
if (!outFile.toPath().startsWith(projectRoot.toPath())) {
105+
warn("Skipping suspicious template entry outside project dir: ${entry.name}")
106+
continue
107+
}
108+
109+
if (entry.isDirectory) {
110+
outFile.mkdirs()
111+
} else {
112+
try {
113+
outFile.parentFile?.mkdirs()
114+
115+
if (entry.name.endsWith(TEMPLATE_EXTENSION)) {
116+
info("Processing template ${entry.name}")
117+
val content = try {
118+
zip.getInputStream(entry).bufferedReader().use { it.readText() }
119+
} catch (e: Exception) {
120+
throw e.wrap("Failed to read template ${entry.name}")
121+
}
122+
123+
val template = try {
124+
pebbleEngine.getTemplate(content)
125+
} catch (e: PebbleException) {
126+
throw e.wrap(
127+
"Pebble parse error in ${entry.name} at line ${e.lineNumber}: ${e.message}"
128+
)
129+
} catch (e: Exception) {
130+
throw e.wrap("Unexpected Pebble parse error in ${entry.name}")
131+
}
132+
133+
val writer = StringWriter()
134+
val rendered = try {
135+
template.evaluate(writer, identifiers)
136+
} catch (e: PebbleException) {
137+
error(
138+
"Pebble evaluation error in ${entry.name} at line ${e.lineNumber}: ${e.message}", e
139+
)
140+
null
141+
} catch (e: Exception) {
142+
error("Unexpected Pebble evaluation error in ${entry.name}", e)
143+
null
144+
}
145+
if (rendered == null) continue
146+
147+
try {
148+
outFile.writeText(writer.toString(), Charsets.UTF_8)
149+
} catch (e: Exception) {
150+
error("Failed writing output file: ${outFile.absolutePath}", e)
151+
}
152+
153+
} else {
154+
try {
155+
zip.getInputStream(entry).use { input ->
156+
outFile.outputStream().use { output ->
157+
input.copyTo(output)
158+
}
159+
}
160+
} catch (e: Exception) {
161+
error("Failed copying binary entry: ${entry.name}", e)
120162
}
121163
}
164+
} catch (e: Exception) {
165+
error("Failed to process template entry: ${entry.name}", e)
122166
}
123167
}
124168
}
125169
}
126170

127171
keystore(executor)
128172

129-
return ProjectTemplateRecipeResultImpl(data)
173+
return ProjectTemplateRecipeResultImpl(data, hasErrorsWarnings)
130174
}
131175

132176
private fun keystore(executor: RecipeExecutor) {
@@ -266,4 +310,20 @@ class ZipRecipeExecutor(
266310
return normalizedParts.joinToString(File.separator)
267311
}
268312

313+
private fun warn(msg: String) {
314+
hasErrorsWarnings = true
315+
log.warn(msg)
316+
}
317+
318+
private fun info(msg: String) {
319+
log.info(msg)
320+
}
321+
322+
private fun error(msg: String, e: Exception) {
323+
hasErrorsWarnings = true
324+
log.error(msg, e)
325+
}
326+
327+
private fun Exception.wrap(msg: String): RuntimeException =
328+
RuntimeException(msg, this)
269329
}

0 commit comments

Comments
 (0)