diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar
index 237f0e5de..f625b502b 100644
Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ
diff --git a/tests/test-project/app/src/main/res/layout/singleView.xml b/tests/test-project/app/src/main/res/layout/singleView.xml
new file mode 100644
index 000000000..ebd55776d
--- /dev/null
+++ b/tests/test-project/app/src/main/res/layout/singleView.xml
@@ -0,0 +1,4 @@
+
+
diff --git a/xml-inflater/build.gradle b/xml-inflater/build.gradle
index 5af3c249b..0a9c36c19 100644
--- a/xml-inflater/build.gradle
+++ b/xml-inflater/build.gradle
@@ -34,6 +34,14 @@ android {
includeAndroidResources = true
}
}
+
+ sourceSets {
+ main {
+ java {
+ srcDir "${buildDir.absolutePath}/generated/source/kaptKotlin/"
+ }
+ }
+ }
}
dependencies {
@@ -44,9 +52,12 @@ dependencies {
implementation project(path: ':build-tools:aaptcompiler')
implementation project(path: ':build-tools:projects')
implementation project(path: ':build-tools:xml-utils')
+ implementation project(path: ':resources')
kapt project(path: ':annotation-processors')
+ testImplementation project (path: ':build-tools:projects')
+ testImplementation project (path: ':build-tools:tooling-api-testing')
testImplementation tests.junit
testImplementation tests.google_truth
testImplementation tests.robolectric
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IAttributeAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IAttributeAdapter.kt
index f59f30498..35872f0b6 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IAttributeAdapter.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IAttributeAdapter.kt
@@ -31,4 +31,11 @@ abstract class IAttributeAdapter {
* @param attribute The attribute to apply.
*/
abstract fun apply(view: IView, attribute: IAttribute)
+
+ /**
+ * Apply the basic attributes to a view so that it could be rendered.
+ *
+ * @param view The view to apply basic attributes to.
+ */
+ abstract fun applyBasic(view: IView)
}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt
index 76d2461af..b973ec738 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/IView.kt
@@ -39,7 +39,7 @@ interface IView {
val view: View
/** The parent view. */
- val parent: IViewGroup
+ var parent: IViewGroup?
/**
* Add and apply the given attribute to this view.
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt
new file mode 100644
index 000000000..9c486a803
--- /dev/null
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ErrorView.kt
@@ -0,0 +1,47 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.inflater.internal
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.itsaky.androidide.inflater.R
+
+/**
+ * View used to show views that cannot be inflated.
+ *
+ * @author Akash Yadav
+ */
+class ErrorView(context: Context, message: String) :
+ ViewImpl(NAME, createErrView(context, message)) {
+ companion object {
+ private val NAME = View::class.java.name
+ }
+}
+
+private fun createErrView(context: Context, message: String): View {
+ return TextView(context).apply {
+ text = message
+ background = ColorDrawable(Color.WHITE)
+ setTextColor(ContextCompat.getColor(context, R.color.secondaryColor))
+ setTextIsSelectable(true)
+ }
+}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt
index 57e6b9797..e8f848166 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt
@@ -17,11 +17,13 @@
package com.itsaky.androidide.inflater.internal
+import android.content.Context
import android.view.InflateException
+import android.view.View
import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
import com.android.aapt.Resources.SourcePosition
import com.android.aapt.Resources.XmlElement
-import com.android.aapt.Resources.XmlNode.NodeCase
import com.android.aapt.Resources.XmlNode.NodeCase.ELEMENT
import com.android.aaptcompiler.AaptResourceType.LAYOUT
import com.android.aaptcompiler.BlameLogger
@@ -33,29 +35,35 @@ import com.itsaky.androidide.aapt.logging.IDELogger
import com.itsaky.androidide.inflater.IInflateEventsListener
import com.itsaky.androidide.inflater.ILayoutInflater
import com.itsaky.androidide.inflater.IView
+import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.InflationFinishEvent
import com.itsaky.androidide.inflater.InflationStartEvent
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
+import com.itsaky.androidide.utils.ILogger
+import com.itsaky.androidide.xml.widgets.Widget
+import com.itsaky.androidide.xml.widgets.WidgetTable
import java.io.File
+import java.lang.reflect.Method
/**
* Default implementation of [ILayoutInflater].
*
* @author Akash Yadav
*/
-internal class LayoutInflaterImpl : ILayoutInflater() {
+open class LayoutInflaterImpl : ILayoutInflater() {
override var inflationEventListener: IInflateEventsListener? = null
+ private val log = ILogger.newInstance("LayoutInflaterImpl")
override fun inflate(file: File, parent: ViewGroup): IView? {
inflationEventListener?.onEvent(InflationStartEvent())
- return doInflate(file, parent).apply {
+ return doInflate(file, wrap(parent)).apply {
inflationEventListener?.onEvent(InflationFinishEvent(this))
}
}
- private fun doInflate(file: File, parent: ViewGroup): IView? {
+ protected open fun doInflate(file: File, parent: IViewGroup): IView? {
val pathData = extractPathData(file)
if (pathData.type != LAYOUT) {
throw InflateException("File is not a layout file.")
@@ -77,30 +85,156 @@ internal class LayoutInflaterImpl : ILayoutInflater() {
val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger))
processor.process(resFile, file.inputStream())
- return doInflate(processor, parent)
+ return doInflate(processor, parent, module)
}
- private fun doInflate(processor: XmlProcessor, parent: ViewGroup): IView? {
+ protected open fun doInflate(
+ processor: XmlProcessor,
+ parent: IViewGroup,
+ module: AndroidModule,
+ ): IView? {
// TODO(itsaky) : Add test for multiple view as root layout
// The inflater should fail in such cases
- val (file, node) =
+ val (_, node) =
processor.xmlResources.find { it.file == processor.primaryFile }
?: throw InflateException("Unable to find primary XML resource from XmlProcessor")
-
+
if (node.nodeCase != ELEMENT) {
- throw InflateException("Found ${node.nodeCase} but $ELEMENT was expected at ${node.source.lineCol()}")
+ throw InflateException(
+ "Found ${node.nodeCase} but $ELEMENT was expected at ${node.source.lineCol()}"
+ )
}
-
+
val element = node.element
- val view = onCreateView(element)
+ return onCreateView(element, parent, module)
+ }
+
+ protected open fun onCreateView(
+ element: XmlElement,
+ parent: IViewGroup,
+ module: AndroidModule,
+ widgets: WidgetTable =
+ module.getWidgetTable()
+ ?: throw IllegalStateException("No widget table found for module $module"),
+ ): IView {
+
+ val parentView = parent.view as ViewGroup
+
+ if ("include" == element.name) {
+ return onCreateIncludedView(element, parentView, module, widgets)
+ }
+
+ if ("merge" == element.name) {
+ return onCreateMergedView(element, parentView, module, widgets)
+ }
+
+ val widget =
+ if (element.name.contains('.')) widgets.getWidget(element.name)
+ else widgets.findWidgetWithSimpleName(element.name)
+
+ // TODO(itsaky): Handle views from libraries
+ val view =
+ if (widget == null) {
+ onCreateUnsupportedView("View with name '${element.name}' not found", parentView)
+ } else {
+ onCreatePlatformView(widget, parentView, module, widgets)
+ }
+
+ val adapter =
+ AttributeAdapterIndex.getAdapter(view.name)
+ ?: throw InflateException("No attribute adapter found for view ${view.name}")
+
+ adapter.applyBasic(view)
+
+ view.view.layoutParams = generateLayoutParams(parentView)
+ parent.addChild(view)
+
+ if (element.childCount > 0 && view is IViewGroup) {
+ for (child in element.childList) {
+ if (child.nodeCase != ELEMENT) {
+ throw InflateException("Unexpected node at ${child.source.lineCol()}")
+ }
+ val childView =
+ onCreateView(element = child.element, parent = view, module = module, widgets = widgets)
+ view.addChild(childView)
+ }
+ }
+
+ return view
+ }
+
+ private fun onCreateMergedView(
+ element: XmlElement,
+ parent: ViewGroup,
+ module: AndroidModule,
+ widgets: WidgetTable,
+ ): IView {
TODO("Not yet implemented")
}
-
- private fun onCreateView(element: XmlElement): IView {
+
+ private fun onCreateIncludedView(
+ element: XmlElement,
+ parent: ViewGroup,
+ module: AndroidModule,
+ widgets: WidgetTable,
+ ): IView {
TODO("Not yet implemented")
}
+
+ protected open fun onCreatePlatformView(
+ widget: Widget,
+ parent: ViewGroup,
+ module: AndroidModule,
+ widgets: WidgetTable,
+ ): IView {
+ return try {
+ val v = createViewInstance(widget, parent)
+ return ViewImpl(widget.qualifiedName, v)
+ } catch (err: Throwable) {
+ onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent)
+ }
+ }
+
+ protected open fun generateLayoutParams(parent: ViewGroup): LayoutParams {
+ return try {
+ var clazz: Class = parent.javaClass
+ var method: Method?
+ while (true) {
+ try {
+ method = clazz.getDeclaredMethod("generateDefaultLayoutParams")
+ break
+ } catch (e: Throwable) {
+ /* ignored */
+ }
+
+ clazz = clazz.superclass
+ }
+ if (method != null) {
+ method.isAccessible = true
+ return method.invoke(parent) as LayoutParams
+ }
+ log.error("Unable to create default params for view parent:", parent)
+ LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
+ } catch (th: Throwable) {
+ throw InflateException("Unable to create layout params for parent: $parent", th)
+ }
+ }
+
+ private fun createViewInstance(widget: Widget, parent: ViewGroup): View {
+ val klass = javaClass.classLoader!!.loadClass(widget.qualifiedName)
+ val constructor = klass.getConstructor(Context::class.java)
+ return constructor.newInstance(parent.context) as View
+ }
+
+ private fun onCreateUnsupportedView(message: String, parent: ViewGroup): IView {
+ return ErrorView(parent.context, message)
+ }
- private fun SourcePosition.lineCol() : String {
+ private fun wrap(parent: ViewGroup) : IViewGroup {
+ return ViewGroupImpl(parent.javaClass.name, parent)
+ }
+
+ private fun SourcePosition.lineCol(): String {
return "line $lineNumber and column $columnNumber"
}
}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AbstractViewGroup.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt
similarity index 93%
rename from xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AbstractViewGroup.kt
rename to xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt
index bdb76ed2a..3feed025b 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AbstractViewGroup.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt
@@ -21,8 +21,8 @@ import android.view.ViewGroup
import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.IViewGroup
-abstract class AbstractViewGroup(name: String, view: ViewGroup) :
- AbstractView(name = name, view = view), IViewGroup {
+open class ViewGroupImpl(name: String, view: ViewGroup) :
+ ViewImpl(name = name, view = view), IViewGroup {
protected val children = mutableListOf()
override val childCount: Int
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AbstractView.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt
similarity index 66%
rename from xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AbstractView.kt
rename to xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt
index ed0441d58..a95ab484d 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/AbstractView.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt
@@ -20,18 +20,36 @@ package com.itsaky.androidide.inflater.internal
import android.view.View
import com.itsaky.androidide.inflater.IAttribute
import com.itsaky.androidide.inflater.IView
+import com.itsaky.androidide.inflater.IViewGroup
import com.itsaky.androidide.inflater.internal.utils.simpleName
import com.itsaky.androidide.inflater.internal.utils.tagName
+import com.itsaky.androidide.utils.ILogger
-abstract class AbstractView(
+open class ViewImpl
+@JvmOverloads
+constructor(
override val name: String,
override val view: View,
override val simpleName: String = name.simpleName(),
override val tag: String = name.tagName()
) : IView {
+ private val log = ILogger.newInstance(javaClass.simpleName)
protected val attributes = mutableListOf()
+ override var parent: IViewGroup? = null
+
override fun addAttribute(attribute: IAttribute) {
-
+ this.attributes.add(attribute)
+ val adapter = AttributeAdapterIndex.getAdapter(name)
+ if (adapter == null) {
+ log.warn("No attribute adapter found for view $name")
+ return
+ }
+ adapter.apply(this, attribute)
+ }
+
+ override fun removeAttribute(attribute: IAttribute) {
+ this.attributes.remove(attribute)
+ // TODO(itsaky): Should attribute adapters handle this as well?
}
}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt
index c9a94611f..72d4ab653 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewAttrAdapter.kt
@@ -34,4 +34,8 @@ open class ViewAttrAdapter : IAttributeAdapter() {
override fun apply(view: IView, attribute: IAttribute) {
TODO("Not yet implemented")
}
+
+ override fun applyBasic(view: IView) {
+ TODO("Not yet implemented")
+ }
}
diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAttrAdapter.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAttrAdapter.kt
index aaeec5775..03a69f171 100644
--- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAttrAdapter.kt
+++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/adapters/ViewGroupAttrAdapter.kt
@@ -25,7 +25,7 @@ import com.itsaky.androidide.inflater.IView
/** @author Akash Yadav */
@AttributeAdapter(ViewGroup::class)
-class ViewGroupAttrAdapter : IAttributeAdapter() {
+class ViewGroupAttrAdapter : ViewAttrAdapter() {
override fun apply(view: IView, attribute: IAttribute) {
TODO("Not yet implemented")
diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ExampleUnitTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ExampleUnitTest.kt
deleted file mode 100644
index bcb187e38..000000000
--- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ExampleUnitTest.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * This file is part of AndroidIDE.
- *
- * AndroidIDE is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * AndroidIDE is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with AndroidIDE. If not, see .
- */
-
-package com.itsaky.androidide.inflater
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt
new file mode 100644
index 000000000..72cf6e733
--- /dev/null
+++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt
@@ -0,0 +1,69 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.inflater
+
+import android.widget.LinearLayout
+import androidx.appcompat.app.AppCompatActivity
+import com.google.common.truth.Truth.assertThat
+import com.itsaky.androidide.lookup.Lookup
+import com.itsaky.androidide.projects.ProjectManager
+import com.itsaky.androidide.projects.builder.BuildService
+import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage
+import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
+import java.io.File
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.android.controller.ActivityController
+
+/** @author Akash Yadav */
+@RunWith(RobolectricTestRunner::class)
+class LayoutInflaterTest {
+
+ private lateinit var activityController: ActivityController
+ private lateinit var activity: AppCompatActivity
+
+ @Before
+ fun setup() {
+
+ val (server, project) = ToolingApiTestLauncher().launchServer(implDir = "../build-tools/tooling-api-impl")
+ server.initialize(InitializeProjectMessage(File("../tests/test-project").absolutePath)).get()
+
+ Lookup.DEFAULT.register(BuildService.KEY_PROJECT_PROXY, project)
+ ProjectManager.setupProject()
+
+ this.activityController = Robolectric.buildActivity(AppCompatActivity::class.java)
+ this.activity = this.activityController.get()
+ }
+
+ @Test
+ fun `test functionality`() {
+ val file = layoutFile("singleView")
+ val parent = LinearLayout(activity)
+ val inflater = ILayoutInflater.newInflater()
+ val inflated = inflater.inflate(file, parent)
+ assertThat(inflated).isNotNull()
+ }
+
+ private fun layoutFile(name: String): File {
+ val app = ProjectManager.app ?: throw IllegalStateException("Project is not initialized")
+ return File(app.projectDir, "src/main/res/layout/$name.xml")
+ }
+}