diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index 16c463d6d..530501113 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/include.xml b/tests/test-project/app/src/main/res/layout/include.xml new file mode 100644 index 000000000..3c92d4f98 --- /dev/null +++ b/tests/test-project/app/src/main/res/layout/include.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/tests/test-project/app/src/main/res/layout/inlined.xml b/tests/test-project/app/src/main/res/layout/inlined.xml new file mode 100644 index 000000000..4d8727910 --- /dev/null +++ b/tests/test-project/app/src/main/res/layout/inlined.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/tests/test-project/app/src/main/res/layout/layout_include.xml b/tests/test-project/app/src/main/res/layout/layout_include.xml new file mode 100644 index 000000000..1b75662a9 --- /dev/null +++ b/tests/test-project/app/src/main/res/layout/layout_include.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/tests/test-project/app/src/main/res/layout/singleView.xml b/tests/test-project/app/src/main/res/layout/singleView.xml index ebd55776d..798ae91d9 100644 --- a/tests/test-project/app/src/main/res/layout/singleView.xml +++ b/tests/test-project/app/src/main/res/layout/singleView.xml @@ -1,4 +1,5 @@ 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 index 16cf2d8d8..919a769a0 100644 --- 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 @@ -23,6 +23,7 @@ import android.graphics.drawable.ColorDrawable import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat +import com.android.aaptcompiler.XmlResource import com.itsaky.androidide.inflater.R import java.io.File @@ -31,7 +32,7 @@ import java.io.File * * @author Akash Yadav */ -class ErrorView(file: File, context: Context, message: String) : +class ErrorView(file: LayoutFile, context: Context, message: String) : ViewImpl(file = file, name = NAME, view = createErrView(context, message)) { companion object { private val NAME = View::class.java.name diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutFile.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutFile.kt new file mode 100644 index 000000000..f0d4b7852 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutFile.kt @@ -0,0 +1,30 @@ +/* + * 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 java.io.File + +/** + * A data class to hold information about the file which was used to inflate and create a [ViewImpl] + * instance. + * + * @property file The actual file. + * @property resName The resource name. + * @author Akash Yadav + */ +data class LayoutFile(val file: File, val resName: String) 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 777467ea4..cfc797d2c 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 @@ -19,12 +19,12 @@ package com.itsaky.androidide.inflater.internal import android.content.Context import android.view.View -import com.itsaky.androidide.inflater.InflateException 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.ELEMENT +import com.android.aaptcompiler.AaptResourceType.ID import com.android.aaptcompiler.AaptResourceType.LAYOUT import com.android.aaptcompiler.BlameLogger import com.android.aaptcompiler.ResourceFile @@ -36,8 +36,10 @@ 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.InflateException import com.itsaky.androidide.inflater.InflationFinishEvent import com.itsaky.androidide.inflater.InflationStartEvent +import com.itsaky.androidide.inflater.internal.utils.IDTable import com.itsaky.androidide.projects.ProjectManager import com.itsaky.androidide.projects.api.AndroidModule import com.itsaky.androidide.utils.ILogger @@ -55,17 +57,22 @@ open class LayoutInflaterImpl : ILayoutInflater() { override var inflationEventListener: IInflateEventsListener? = null private val log = ILogger.newInstance("LayoutInflaterImpl") - private var inflatingFile: File? = null + private var _primaryInflatingFile: File? = null + private var _currentLayoutFile: LayoutFile? = null + + protected val primaryInflatingFile: File + get() = this._primaryInflatingFile!! - protected val file: File - get() = this.inflatingFile!! + protected val currentLayoutFile: LayoutFile + get() = _currentLayoutFile!! override fun inflate(file: File, parent: ViewGroup): IView? { - this.inflatingFile = file + this._primaryInflatingFile = file + IDTable.newRound() inflationEventListener?.onEvent(InflationStartEvent()) return doInflate(file, wrap(parent)).apply { inflationEventListener?.onEvent(InflationFinishEvent(this)) - inflatingFile = null + _primaryInflatingFile = null } } @@ -90,7 +97,7 @@ open class LayoutInflaterImpl : ILayoutInflater() { val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger)) processor.process(resFile, file.inputStream()) - + return doInflate(processor, parent, module) } @@ -101,18 +108,29 @@ open class LayoutInflaterImpl : ILayoutInflater() { ): IView? { // TODO(itsaky) : Add test for multiple view as root layout // The inflater should fail in such cases - val (_, node) = + val (file, node) = processor.xmlResources.find { it.file == processor.primaryFile } ?: throw InflateException("Unable to find primary XML resource from XmlProcessor") - + + this._currentLayoutFile = LayoutFile(this.primaryInflatingFile, file.name.entry!!) + if (node.nodeCase != ELEMENT) { throw InflateException( "Found ${node.nodeCase} but $ELEMENT was expected at ${node.source.lineCol()}" ) } - + + // Store all IDs + file.exportedSymbols.filter { it.name.type == ID }.forEach { + IDTable.set(currentLayoutFile.resName, it.name.entry!!, View.generateViewId()) + } + val element = node.element - return onCreateView(element, parent, module) + val view = onCreateView(element, parent, module) + + this._currentLayoutFile = null + + return view } protected open fun onCreateView( @@ -195,7 +213,7 @@ open class LayoutInflaterImpl : ILayoutInflater() { ): IView { return try { val v = createViewInstance(widget, parent) - return ViewImpl(file, widget.qualifiedName, v) + return ViewImpl(currentLayoutFile, widget.qualifiedName, v) } catch (err: Throwable) { onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent) } @@ -233,11 +251,11 @@ open class LayoutInflaterImpl : ILayoutInflater() { } private fun onCreateUnsupportedView(message: String, parent: ViewGroup): IView { - return ErrorView(file, parent.context, message) + return ErrorView(currentLayoutFile, parent.context, message) } private fun wrap(parent: ViewGroup): IViewGroup { - return ViewGroupImpl(file, parent.javaClass.name, parent) + return ViewGroupImpl(currentLayoutFile, parent.javaClass.name, parent) } private fun SourcePosition.lineCol(): String { diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt index d3d3050dc..a62323ddd 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewGroupImpl.kt @@ -18,11 +18,12 @@ package com.itsaky.androidide.inflater.internal import android.view.ViewGroup +import com.android.aaptcompiler.XmlResource import com.itsaky.androidide.inflater.IView import com.itsaky.androidide.inflater.IViewGroup import java.io.File -open class ViewGroupImpl(file: File, name: String, view: ViewGroup) : +open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) : ViewImpl(file = file, name = name, view = view), IViewGroup { protected val children = mutableListOf() diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt index 4c4f7b255..eedb3432a 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt @@ -18,6 +18,7 @@ package com.itsaky.androidide.inflater.internal import android.view.View +import com.android.aaptcompiler.XmlResource import com.itsaky.androidide.inflater.IAttribute import com.itsaky.androidide.inflater.IView import com.itsaky.androidide.inflater.IViewGroup @@ -29,7 +30,7 @@ import java.io.File open class ViewImpl @JvmOverloads constructor( - internal val file: File, + internal val file: LayoutFile, override val name: String, override val view: View, override val simpleName: String = name.simpleName(), 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 863221be0..cf5626b80 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 @@ -29,13 +29,14 @@ import com.itsaky.androidide.inflater.IAttribute import com.itsaky.androidide.inflater.IAttributeAdapter import com.itsaky.androidide.inflater.INamespace import com.itsaky.androidide.inflater.IView +import com.itsaky.androidide.inflater.internal.LayoutFile import com.itsaky.androidide.inflater.internal.ViewImpl import com.itsaky.androidide.inflater.internal.utils.endParse +import com.itsaky.androidide.inflater.internal.utils.parseId import com.itsaky.androidide.inflater.internal.utils.parseString import com.itsaky.androidide.inflater.internal.utils.startParse import com.itsaky.androidide.projects.ProjectManager import com.itsaky.androidide.projects.api.AndroidModule -import java.io.File /** * Attribute adapter for [View]. @@ -69,7 +70,7 @@ open class ViewAttrAdapter : IAttributeAdapter() { "foregroundGravity" -> foregroundGravity = parseGravity(value) "foregroundTint" -> foregroundTintList = parseColorStateList(context, value) "foregroundTintMode" -> foregroundTintMode = parsePorterDuffMode(value) - "id" -> {} + "id" -> parseId(file.resName, value) "minHeight" -> minimumWidth = parseDimension(context, value) "minWidth" -> minimumHeight = parseDimension(context, value) "padding" -> @@ -127,7 +128,7 @@ open class ViewAttrAdapter : IAttributeAdapter() { attribute: IAttribute, block: T.( - file: File, + file: LayoutFile, context: Context, layoutParams: ViewGroup.LayoutParams, namespace: INamespace, @@ -141,7 +142,7 @@ open class ViewAttrAdapter : IAttributeAdapter() { (view.view as T).apply { val file = (view as ViewImpl).file val module = - ProjectManager.findModuleForFile(file) as? AndroidModule + ProjectManager.findModuleForFile(file.file) as? AndroidModule ?: throw IllegalStateException("Cannot find module for file: $file") startParse(module) block(file, context, layoutParams, attribute.namespace, attribute.name, attribute.value) diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/IDTable.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/IDTable.kt new file mode 100644 index 000000000..43f75df0e --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/IDTable.kt @@ -0,0 +1,35 @@ +/* + * 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.utils + +internal object IDTable { + + private val ids = mutableMapOf>() + + fun newRound() { + ids.clear() + } + + fun get(file: String, name: String): Int? { + return ids[file]?.get(name) + } + + fun set(file: String, name: String, id: Int) { + ids.computeIfAbsent(file) { mutableMapOf() }[name] = id + } +} diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt index 7400d37fd..1a0e165df 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/basicParseUtils.kt @@ -440,7 +440,7 @@ fun resolveResourceReference( } } -private fun parseResourceReference(value: String): Triple? { +internal fun parseResourceReference(value: String): Triple? { return tryParseReference(value)?.let { val pck = it.reference.name.pck val type = it.reference.name.type diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/viewParseUtils.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/viewParseUtils.kt index d51c650c6..8fb287b1b 100644 --- a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/viewParseUtils.kt +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/viewParseUtils.kt @@ -19,9 +19,13 @@ package com.itsaky.androidide.inflater.internal.utils import android.graphics.PorterDuff import android.view.View +import com.android.aaptcompiler.AaptResourceType.ID +import com.itsaky.androidide.inflater.internal.ViewImpl -fun parseId(value: String) : Int { - return 0 +@JvmOverloads +fun parseId(resName: String, value: String, def: Int = 0): Int { + val name = parseResourceReference(value)?.third ?: return 0 + return IDTable.get(resName, name) ?: def } @Suppress("DEPRECATION") 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 index 1270c7d08..2ebd6e74f 100644 --- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt +++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/LayoutInflaterTest.kt @@ -53,6 +53,22 @@ class LayoutInflaterTest { } } } + + @Test + fun `test inlined layout`() { + inflaterTest { + requiresActivity { activity -> + val file = layoutFile("inlined") + val parent = LinearLayout(activity) + val inflater = ILayoutInflater.newInflater() + val inflated = inflater.inflate(file, parent) + inflated.apply { + assertThat(this).isNotNull() + assertThat(this).isInstanceOf(LinearLayout::class.java) + } + } + } + } private fun layoutFile(name: String): File { val app = ProjectManager.app ?: throw IllegalStateException("Project is not initialized")