diff --git a/annotation-processors/src/main/java/com/itsaky/androidide/annotations/inflater/AttrAdapterAnnotationProcessor.kt b/annotation-processors/src/main/java/com/itsaky/androidide/annotations/inflater/AttrAdapterAnnotationProcessor.kt index 1f790bdb7..f4af80aca 100644 --- a/annotation-processors/src/main/java/com/itsaky/androidide/annotations/inflater/AttrAdapterAnnotationProcessor.kt +++ b/annotation-processors/src/main/java/com/itsaky/androidide/annotations/inflater/AttrAdapterAnnotationProcessor.kt @@ -186,7 +186,7 @@ class AttrAdapterAnnotationProcessor : AbstractProcessor() { private fun getViewName(annotation: AttributeAdapter): String { return try { - annotation.forClass.qualifiedName + annotation.forView.qualifiedName ?: throw IllegalStateException("Cannot find type of $annotation") } catch (err: MirroredTypeException) { (processingEnv.typeUtils.asElement(err.typeMirror) as TypeElement).qualifiedName.toString() diff --git a/annotations/src/main/java/com/itsaky/androidide/annotations/inflater/AttributeAdapter.kt b/annotations/src/main/java/com/itsaky/androidide/annotations/inflater/AttributeAdapter.kt index 20f5a0a5b..1d6307201 100644 --- a/annotations/src/main/java/com/itsaky/androidide/annotations/inflater/AttributeAdapter.kt +++ b/annotations/src/main/java/com/itsaky/androidide/annotations/inflater/AttributeAdapter.kt @@ -27,4 +27,4 @@ import kotlin.reflect.KClass */ @Target(CLASS) @Retention(AnnotationRetention.SOURCE) -annotation class AttributeAdapter(val forClass: KClass) +annotation class AttributeAdapter(val forView: KClass) diff --git a/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt b/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt index 0e7cc7817..753c5c067 100644 --- a/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt +++ b/build-tools/projects/src/main/java/com/itsaky/androidide/projects/api/AndroidModule.kt @@ -17,6 +17,8 @@ package com.itsaky.androidide.projects.api +import com.android.SdkConstants +import com.android.aaptcompiler.AaptResourceType import com.android.aaptcompiler.ResourceTable import com.android.builder.model.v2.ide.LibraryType.ANDROID_LIBRARY import com.android.builder.model.v2.ide.LibraryType.JAVA_LIBRARY @@ -119,7 +121,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this.. private val log = ILogger.newInstance(javaClass.simpleName) override var moduleData: SimpleModuleData? = null - + init { type = Android } @@ -304,15 +306,13 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this.. val resDirs = mainSourceSet?.sourceProvider?.resDirectories ?: return null return ResourceTableRegistry.getInstance().forPackage(this.packageName, *resDirs.toTypedArray()) } - - /** - * Updates the resource table for this module. - */ + + /** Updates the resource table for this module. */ fun updateResourceTable() { if (this.packageName == UNKNOWN_PACKAGE) { return } - + CompletableFuture.runAsync { val tableRegistry = ResourceTableRegistry.getInstance() val resDirs = mainSourceSet?.sourceProvider?.resDirectories ?: return@runAsync @@ -369,6 +369,52 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this.. } } + /** + * Checks all the resource tables from this module and returns if any of the tables contain + * resources for the the given package. + * + * @param pck The package to look for. + */ + fun findResourceTableForPackage(pck: String, hasGroup: AaptResourceType? = null): ResourceTable? { + if (pck == SdkConstants.ANDROID_PKG) { + return getFrameworkResourceTable() + } + + val tables: List = + mutableListOf().apply { + getResourceTable()?.let { add(it) } + addAll(getSourceResourceTables()) + addAll(getDependencyResourceTables()) + } + + for (table in tables) { + val resPck = table.findPackage(pck) ?: continue + if (hasGroup == null) { + return table + } + if (resPck.findGroup(hasGroup) != null) { + return table + } + } + + return null + } + + /** + * Returns all the resource tables associated with this module (including the framework resource + * table). + * + * @return The associated resource tables. + */ + fun getAllResourceTables(): Set { + return mutableSetOf().apply { + getResourceTable()?.let { add(it) } + getFrameworkResourceTable()?.let { add(it) } + addAll(getSourceResourceTables()) + addAll(getDependencyResourceTables()) + } + } + /** Get the resource table for the attrs_manifest.xml file. */ fun getManifestAttrTable(): ResourceTable? { val platform = getPlatformDir() ?: return null diff --git a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/patterns.kt b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/patterns.kt new file mode 100644 index 000000000..dc89aa792 --- /dev/null +++ b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/utils/patterns.kt @@ -0,0 +1,25 @@ +/* + * 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.xml.utils + +import java.util.regex.Pattern + +val attrValue_unqualifiedRef: Pattern by lazy { Pattern.compile("@(\\w+)/(.*)") } +val attrValue_qualifiedRef: Pattern by lazy { Pattern.compile("@((\\w|\\.)+):(\\w+)/(.*)") } +val attrValue_qualifiedRefWithIncompleteType: Pattern by lazy { Pattern.compile("@((\\w|\\.)+):(\\w*)") } +val attrValue_qualifiedRefWithIncompletePckOrType: Pattern by lazy { Pattern.compile("@((\\w|\\.)*)") } \ No newline at end of file diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrValueCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrValueCompletionProvider.kt index ddab19aae..8d611e6e5 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrValueCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/AttrValueCompletionProvider.kt @@ -49,7 +49,10 @@ import com.itsaky.androidide.lsp.xml.edits.QualifiedValueEditHandler import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE_VALUE import com.itsaky.androidide.xml.resources.ResourceTableRegistry -import java.util.regex.Pattern +import com.itsaky.androidide.xml.utils.attrValue_qualifiedRef +import com.itsaky.androidide.xml.utils.attrValue_qualifiedRefWithIncompletePckOrType +import com.itsaky.androidide.xml.utils.attrValue_qualifiedRefWithIncompleteType +import com.itsaky.androidide.xml.utils.attrValue_unqualifiedRef import org.eclipse.lemminx.dom.DOMDocument /** @@ -60,11 +63,6 @@ import org.eclipse.lemminx.dom.DOMDocument open class AttrValueCompletionProvider(provider: ICompletionProvider) : IXmlCompletionProvider(provider) { - private val unqualifiedRefMatcher by lazy { Pattern.compile("@(\\w+)/(.*)") } - private val qualifiedRef by lazy { Pattern.compile("@((\\w|\\.)+):(\\w+)/(.*)") } - private val qualifiedRefWithIncompleteType by lazy { Pattern.compile("@((\\w|\\.)+):(\\w*)") } - private val qualifiedRefWithIncompletePckOrType by lazy { Pattern.compile("@((\\w|\\.)*)") } - override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean { return super.canProvideCompletions(pathData, type) && type == ATTRIBUTE_VALUE } @@ -121,7 +119,7 @@ open class AttrValueCompletionProvider(provider: ICompletionProvider) : // If user is typign entry with package name and resource type. For example // '@com.itsaky.test.app:string/app_name' or '@android:string/ok' - var matcher = qualifiedRef.matcher(value) + var matcher = attrValue_qualifiedRef.matcher(value) if (matcher.matches()) { val valPck = matcher.group(1) val typeStr = matcher.group(3) @@ -133,7 +131,7 @@ open class AttrValueCompletionProvider(provider: ICompletionProvider) : // If user is typing qualified reference but with incomplete type // For example: '@android:str' or '@com.itsaky.test.app:str' - matcher = qualifiedRefWithIncompleteType.matcher(value) + matcher = attrValue_qualifiedRefWithIncompleteType.matcher(value) if (matcher.matches()) { val valPck = matcher.group(1)!! val incompleteType = matcher.group(3) ?: "" @@ -143,7 +141,7 @@ open class AttrValueCompletionProvider(provider: ICompletionProvider) : // If user is typing qualified reference but with incomplete type or package name // For example: '@android:str' or '@str' - matcher = qualifiedRefWithIncompletePckOrType.matcher(value) + matcher = attrValue_qualifiedRefWithIncompletePckOrType.matcher(value) if (matcher.matches()) { val valPck = matcher.group(1)!! @@ -157,7 +155,7 @@ open class AttrValueCompletionProvider(provider: ICompletionProvider) : } // If user is typing entry name with resource type. For example '@string/app_name' - matcher = unqualifiedRefMatcher.matcher(value) + matcher = attrValue_unqualifiedRef.matcher(value) if (matcher.matches()) { val typeStr = matcher.group(1) val newPrefix = matcher.group(2) ?: "" diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index f625b502b..13c19a957 100644 Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ 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 9c486a803..16cf2d8d8 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 @@ -24,14 +24,15 @@ import android.view.View import android.widget.TextView import androidx.core.content.ContextCompat import com.itsaky.androidide.inflater.R +import java.io.File /** * View used to show views that cannot be inflated. * * @author Akash Yadav */ -class ErrorView(context: Context, message: String) : - ViewImpl(NAME, createErrView(context, message)) { +class ErrorView(file: File, 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/LayoutInflaterImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/LayoutInflaterImpl.kt index e8f848166..f0cf6f82d 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 @@ -55,11 +55,17 @@ open class LayoutInflaterImpl : ILayoutInflater() { override var inflationEventListener: IInflateEventsListener? = null private val log = ILogger.newInstance("LayoutInflaterImpl") + private var inflatingFile: File? = null + + protected val file: File + get() = this.inflatingFile!! override fun inflate(file: File, parent: ViewGroup): IView? { + this.inflatingFile = file inflationEventListener?.onEvent(InflationStartEvent()) return doInflate(file, wrap(parent)).apply { inflationEventListener?.onEvent(InflationFinishEvent(this)) + inflatingFile = null } } @@ -189,7 +195,7 @@ open class LayoutInflaterImpl : ILayoutInflater() { ): IView { return try { val v = createViewInstance(widget, parent) - return ViewImpl(widget.qualifiedName, v) + return ViewImpl(file, widget.qualifiedName, v) } catch (err: Throwable) { onCreateUnsupportedView("Unable to create view for widget ${widget.qualifiedName}", parent) } @@ -227,11 +233,11 @@ open class LayoutInflaterImpl : ILayoutInflater() { } private fun onCreateUnsupportedView(message: String, parent: ViewGroup): IView { - return ErrorView(parent.context, message) + return ErrorView(file, parent.context, message) } - - private fun wrap(parent: ViewGroup) : IViewGroup { - return ViewGroupImpl(parent.javaClass.name, parent) + + private fun wrap(parent: ViewGroup): IViewGroup { + return ViewGroupImpl(file, 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 3feed025b..d3d3050dc 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 @@ -20,9 +20,10 @@ package com.itsaky.androidide.inflater.internal import android.view.ViewGroup import com.itsaky.androidide.inflater.IView import com.itsaky.androidide.inflater.IViewGroup +import java.io.File -open class ViewGroupImpl(name: String, view: ViewGroup) : - ViewImpl(name = name, view = view), IViewGroup { +open class ViewGroupImpl(file: File, name: String, view: ViewGroup) : + ViewImpl(file = file, 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/ViewImpl.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/ViewImpl.kt index a95ab484d..4c4f7b255 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 @@ -24,10 +24,12 @@ 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 +import java.io.File open class ViewImpl @JvmOverloads constructor( + internal val file: File, 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 72d4ab653..5b76af0f8 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 @@ -17,25 +17,90 @@ package com.itsaky.androidide.inflater.internal.adapters +import android.content.Context import android.view.View +import android.view.ViewGroup +import com.android.SdkConstants import com.itsaky.androidide.annotations.inflater.AttributeAdapter 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.ViewImpl +import com.itsaky.androidide.inflater.internal.utils.endParse +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]. * * @author Akash Yadav */ -@AttributeAdapter(forClass = View::class) +@AttributeAdapter(forView = View::class) open class ViewAttrAdapter : IAttributeAdapter() { override fun apply(view: IView, attribute: IAttribute) { - TODO("Not yet implemented") + doApply(view, attribute) { file, context, layoutParams, namespace, name, value -> + var handled = true + when (name) { + "layout_height" -> layoutParams.height = parseDimension(context, value) + "layout_width" -> layoutParams.width = parseDimension(context, value) + "alpha" -> alpha = parseFloat(value) + else -> handled = false + } + this.layoutParams = layoutParams + } + } + + protected fun parseDimension(context: Context, value: String): Int { + return com.itsaky.androidide.inflater.internal.utils.parseDimension(context, value) + } + + protected fun parseFloat(value: String, def: Float = 1f): Float { + return com.itsaky.androidide.inflater.internal.utils.parseFloat(value, def) } override fun applyBasic(view: IView) { TODO("Not yet implemented") } + + protected open fun canHandleNamespace(namespace: INamespace): Boolean { + return this.canHandleNamespace(namespace.uri) + } + protected open fun canHandleNamespace(nsUri: String): Boolean { + return SdkConstants.ANDROID_URI == nsUri + } + + /** + * Provides easy access to various properties related to the view and attribute when applying an + * attributes. + */ + protected fun doApply( + view: IView, + attribute: IAttribute, + block: + T.( + file: File, + context: Context, + layoutParams: ViewGroup.LayoutParams, + namespace: INamespace, + name: String, + value: String + ) -> Unit + ) { + if (!canHandleNamespace(attribute.namespace)) { + return + } + (view.view as T).apply { + val file = (view as ViewImpl).file + val module = + ProjectManager.findModuleForFile(file) as? AndroidModule + ?: throw IllegalStateException("Cannot find module for file: $file") + startParse(module) + block(file, context, layoutParams, attribute.namespace, attribute.name, attribute.value) + endParse() + } + } } diff --git a/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/parseUtils.kt b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/parseUtils.kt new file mode 100644 index 000000000..7a4670cac --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/androidide/inflater/internal/utils/parseUtils.kt @@ -0,0 +1,244 @@ +/* + * 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 + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.TypedValue.COMPLEX_UNIT_DIP +import android.util.TypedValue.COMPLEX_UNIT_IN +import android.util.TypedValue.COMPLEX_UNIT_MM +import android.util.TypedValue.COMPLEX_UNIT_PT +import android.util.TypedValue.COMPLEX_UNIT_PX +import android.util.TypedValue.COMPLEX_UNIT_SP +import android.util.TypedValue.applyDimension +import android.view.ViewGroup.LayoutParams +import com.android.aaptcompiler.AaptResourceType.DIMEN +import com.android.aaptcompiler.BinaryPrimitive +import com.android.aaptcompiler.ConfigDescription +import com.android.aaptcompiler.Reference +import com.android.aaptcompiler.ResourceEntry +import com.android.aaptcompiler.ResourceGroup +import com.android.aaptcompiler.ResourceName +import com.android.aaptcompiler.ResourceTable +import com.android.aaptcompiler.ResourceTablePackage +import com.itsaky.androidide.projects.api.AndroidModule +import com.itsaky.androidide.utils.ILogger +import com.itsaky.androidide.xml.utils.attrValue_qualifiedRef +import com.itsaky.androidide.xml.utils.attrValue_unqualifiedRef +import java.util.regex.Pattern + +private var currentModule: AndroidModule? = null +private val log = ILogger.newInstance("ParseUtilsKt") +private val HEX_COLOR: Pattern = Pattern.compile("#[a-fA-F\\d]{6,8}") + +val module: AndroidModule + get() = + currentModule ?: throw IllegalStateException("You must call startParse(AndroidModule) first") + +fun startParse(m: AndroidModule) { + currentModule = m +} + +fun endParse() { + currentModule = null +} + +fun parseDrawable(context: Context, value: String): Drawable { + if (HEX_COLOR.matcher(value).matches()) { + return parseColorDrawable(value) + } + +// if (value[0] == '@') { +// val (pck, type, name) = parseResourceReference(value) +// return if(pck == null) { +// parseDrawableResRef() +// } else { +// parseQualifiedDrawableResRef() +// } +// } + return newColorDrawable(Color.TRANSPARENT) +} + +fun parseColorDrawable(value: String): Drawable { + val color = + try { + Color.parseColor(value) + } catch (err: Throwable) { + log.warn("Unable to parse color code", value) + Color.TRANSPARENT + } + return newColorDrawable(color) +} + +fun newColorDrawable(color: Int): Drawable { + return ColorDrawable(color) +} + +fun parseDimension( + context: Context, + value: String?, + defValue: Int = LayoutParams.WRAP_CONTENT, +): Int { + if (value.isNullOrBlank()) { + return defValue + } + val c = value[0] + if (c.isDigit()) { + val i = value.length - 2 + val dimension = parseFloat(value.substring(0, i), defValue.toFloat()) + val unit = parseDimensionUnit(value.substring(i)) + return applyDimension(unit, dimension, context.resources.displayMetrics).toInt() + } else if (c == '@') { + val (pck, type, name) = parseResourceReference(value) + return if (pck == null) { + parseDimensionResRef(type, name, value) + } else { + parseQualifiedDimensionResRef(type, value, pck, name) + } + } else { + return when (value) { + "wrap_content" -> LayoutParams.WRAP_CONTENT + "fill_parent", + "match_parent", -> LayoutParams.MATCH_PARENT + else -> { + log.warn("Cannot infer type of dimension resource: '$value'") + LayoutParams.WRAP_CONTENT + } + } + } +} + +fun parseFloat(value: String, defValue: Float): Float { + return try { + value.toFloat() + } catch (err: Throwable) { + defValue + } +} + +fun parseDimensionUnit(unitStr: String): Int { + return when (unitStr) { + "dp" -> COMPLEX_UNIT_DIP + "sp" -> COMPLEX_UNIT_SP + "pt" -> COMPLEX_UNIT_PT + "px" -> COMPLEX_UNIT_PX + "in" -> COMPLEX_UNIT_IN + "mm" -> COMPLEX_UNIT_MM + else -> COMPLEX_UNIT_DIP + } +} + +private fun parseDimensionResRef(type: String, name: String, value: String?): Int { + if (type.isBlank() || name.isBlank()) { + throw IllegalArgumentException("Cannot parse resource reference: '$value'") + } + // Must be a dimension type + if (type != "dimen") { + throw IllegalArgumentException("A dimension value is expect. Current value is $value") + } + + var resTable: ResourceTable? = null + var resGrp: ResourceGroup? = null + var resPck: ResourceTablePackage? = null + var resEntry: ResourceEntry? = null + for (t in module.getAllResourceTables()) { + val result = t.findResource(ResourceName("", DIMEN, name)) ?: continue + resTable = t + resGrp = result.group + resPck = result.tablePackage + resEntry = result.entry + break + } + + return resolveDimensionReference( + table = resTable!!, + group = resGrp!!, + entry = resEntry!!, + pck = resPck!!.name, + name = name + ) +} + +private fun parseQualifiedDimensionResRef( + type: String, + value: String?, + pck: String, + name: String +): Int { + // Must be a dimension type + if (type != "dimen") { + throw IllegalArgumentException("A dimension value is expect. Current value is $value") + } + + val table = + module.findResourceTableForPackage(pck, DIMEN) + ?: throw IllegalArgumentException("Resource table for package '$pck' not found.") + + return resolveDimensionReference(table = table, group = null, pck = pck, name = name) +} + +private fun resolveDimensionReference( + table: ResourceTable, + group: ResourceGroup? = null, + entry: ResourceEntry? = null, + pck: String, + name: String, +): Int { + val grp = group ?: table.findPackage(pck)!!.findGroup(DIMEN)!! + val e = + entry + ?: grp.findEntry(name) + ?: run { + log.warn("Dimension resource with name '$name' not found") + return LayoutParams.WRAP_CONTENT + } + val dimenValue = e.findValue(ConfigDescription())!!.value + if (dimenValue is Reference) { + return resolveDimensionReference( + table = table, + group = grp, + entry = null, + pck = pck, + name = dimenValue.name.entry!! + ) + } else if (dimenValue is BinaryPrimitive) { + return dimenValue.resValue.data + } + + log.warn("Unable to resolve dimension reference '$name'") + return LayoutParams.WRAP_CONTENT +} + +private fun parseResourceReference(value: String): Triple { + // A qualified resource reference + // For example: '@com.itsaky.androidide.resources:dimen/my_dimen' or '@android:dimen/my_dimen' + // ^pck ^type ^name + var matcher = attrValue_qualifiedRef.matcher(value) + if (matcher.matches()) { + return Triple(matcher.group(1)!!, matcher.group(3)!!, matcher.group(4)!!) + } + + matcher = attrValue_unqualifiedRef.matcher(value) + if (matcher.matches()) { + return Triple(null, matcher.group(1)!!, matcher.group(2)!!) + } + + return Triple(null, "", "") +} 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 72cf6e733..1270c7d08 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 @@ -17,6 +17,7 @@ package com.itsaky.androidide.inflater +import android.view.View import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import com.google.common.truth.Truth.assertThat @@ -36,30 +37,21 @@ 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() + inflaterTest { + requiresActivity { activity -> + val file = layoutFile("singleView") + val parent = LinearLayout(activity) + val inflater = ILayoutInflater.newInflater() + val inflated = inflater.inflate(file, parent) + inflated.apply { + assertThat(this).isNotNull() + assertThat(this).isInstanceOf(View::class.java) + } + } + } } private fun layoutFile(name: String): File { diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt new file mode 100644 index 000000000..eee535e31 --- /dev/null +++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/ValueParsersTest.kt @@ -0,0 +1,82 @@ +/* + * 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.graphics.Color +import android.graphics.drawable.ColorDrawable +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.inflater.internal.utils.endParse +import com.itsaky.androidide.inflater.internal.utils.parseDimension +import com.itsaky.androidide.inflater.internal.utils.parseDrawable +import com.itsaky.androidide.inflater.internal.utils.startParse +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ValueParsersTest { + + @Test + fun `hardcoded dimension parser test`() { + inflaterTest { module -> + requiresActivity { activity -> + startParse(module) + val units = arrayOf("dp", "sp", "pt", "px", "in", "mm") + val results = intArrayOf(1, 1, 2, 1, 160, 6) + for (i in units.indices) { + assertThat(parseDimension(activity, "1" + units[i], -1)).isEqualTo(results[i]) + } + endParse() + } + } + } + + @Test + fun `reference dimension parser test`() { + inflaterTest { module -> + requiresActivity { activity -> + startParse(module) + assertThat(parseDimension(activity, "@android:dimen/app_icon_size")).isEqualTo(12289) + assertThat(parseDimension(activity, "@android:dimen/status_bar_height_portrait")) + .isEqualTo(6145) + + // The app module here targets android 31 and the dimension resource below was added in + // android 31. So, the resource should not be resolved and + // ViewGroup.LayoutParams.WRAP_CONTENT must be returned instead. + assertThat(parseDimension(activity, "@android:dimen/status_bar_height_default")) + .isEqualTo(-2) + endParse() + } + } + } + + @Test + fun `simple color drawable parse test`() { + inflaterTest { module -> + requiresActivity { activity -> + startParse(module) + parseDrawable(activity, "#ff0000").apply { + assertThat(this).isNotNull() + assertThat(this).isInstanceOf(ColorDrawable::class.java) + assertThat((this as ColorDrawable).color).isEqualTo(Color.RED) + } + endParse() + } + } + } +} diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt new file mode 100644 index 000000000..1fdf358c0 --- /dev/null +++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt @@ -0,0 +1,59 @@ +/* + * 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 androidx.appcompat.app.AppCompatActivity +import com.itsaky.androidide.lookup.Lookup +import com.itsaky.androidide.projects.ProjectManager +import com.itsaky.androidide.projects.api.AndroidModule +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.Ignore +import org.robolectric.Robolectric + +@Ignore("Test utility provider") +object XmlInflaterTest { + + private var init: Boolean = false + internal val activity by lazy { Robolectric.buildActivity(AppCompatActivity::class.java).get() } + + fun initIfNeeded() { + if (init) { + return + } + + 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() + init = true + } +} + +fun inflaterTest(block: (AndroidModule) -> Unit) { + XmlInflaterTest.initIfNeeded() + block(ProjectManager.app!!) +} + +fun requiresActivity(block: (AppCompatActivity) -> Unit) { + block(XmlInflaterTest.activity) +}