feat: added support for <include> and <merge> tags

This commit is contained in:
Akash Yadav
2022-11-19 08:52:07 +05:30
parent 5da73f110f
commit 87ae3a5cb6
17 changed files with 285 additions and 135 deletions
Binary file not shown.
@@ -4,6 +4,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/layout_include"/>
<include layout="@layout/layout_include" android:layout_width="48dp" android:layout_height="48dp"/>
</LinearLayout>
@@ -5,17 +5,6 @@
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:id="@+id/included_text"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="This is an included layout"/>
<TextView
android:id="@+id/included_text_other"
android:layout_below="@id/included_text"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="This is an included layout"/>
<include layout="@layout/layout_merge"/>
</RelativeLayout>
@@ -0,0 +1,31 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/included_text"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="This is an included layout"/>
<TextView
android:id="@+id/included_text_other"
android:layout_below="@id/included_text"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="This is an included layout"/>
</merge>
@@ -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 <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/merge_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/layout_merge" android:layout_width="48dp" android:layout_height="48dp"/>
</LinearLayout>
@@ -114,4 +114,4 @@ class DesignerWorkspaceFragment : BaseFragment() {
viewModel.file = file
}
}
}
}
@@ -37,7 +37,7 @@ abstract class ILayoutInflater {
* @param file The file to inflate.
* @param parent The parent view.
*/
abstract fun inflate(file: File, parent: ViewGroup): IView?
abstract fun inflate(file: File, parent: ViewGroup): List<IView>
companion object {
@JvmStatic
@@ -47,7 +47,7 @@ class InflationStartEvent : IInflationEvent<Unit>(Unit)
*
* @property view The inflated view.
*/
class InflationFinishEvent(view: IView?) : IInflationEvent<IView?>(view)
class InflationFinishEvent(views: List<IView>) : IInflationEvent<List<IView>>(views)
/**
* Indicates that the given view has been inflated. The view can be anywhere in the layout.
@@ -21,13 +21,16 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import com.android.SdkConstants.EXT_XML
import com.android.aapt.Resources.SourcePosition
import com.android.aapt.Resources.XmlAttribute
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
import com.android.aaptcompiler.ResourceFile.Type.ProtoXml
import com.android.aaptcompiler.ResourceName
import com.android.aaptcompiler.XmlProcessor
import com.android.aaptcompiler.extractPathData
@@ -42,6 +45,7 @@ import com.itsaky.androidide.inflater.InflationStartEvent
import com.itsaky.androidide.inflater.OnApplyAttributeEvent
import com.itsaky.androidide.inflater.OnInflateViewEvent
import com.itsaky.androidide.inflater.internal.utils.IDTable
import com.itsaky.androidide.inflater.internal.utils.parseLayoutReference
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.utils.ILogger
@@ -68,7 +72,7 @@ open class LayoutInflaterImpl : ILayoutInflater() {
protected val currentLayoutFile: LayoutFile
get() = this._currentLayoutFile!!
override fun inflate(file: File, parent: ViewGroup): IView? {
override fun inflate(file: File, parent: ViewGroup): List<IView> {
this._primaryInflatingFile = file
IDTable.newRound()
inflationEventListener?.onEvent(InflationStartEvent())
@@ -78,32 +82,8 @@ open class LayoutInflaterImpl : ILayoutInflater() {
}
}
protected open fun doInflate(file: File, parent: ViewGroup): IView? {
val pathData = extractPathData(file)
if (pathData.type != LAYOUT) {
throw InflateException("File is not a layout file.")
}
if (ProjectManager.rootProject == null) {
throw InflateException("Project is not initialized!")
}
val module =
ProjectManager.findModuleForFile(file) as? AndroidModule
?: throw InflateException(
"Cannot find module for given file. Is the project initialized?"
)
val resFile =
ResourceFile(
ResourceName(module.packageName, pathData.type!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.ProtoXml
)
val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger))
processor.process(resFile, file.inputStream())
protected open fun doInflate(file: File, parent: ViewGroup): List<IView> {
val (processor, module) = processXmlFile(file)
return doInflate(processor, parent, module)
}
@@ -111,7 +91,15 @@ open class LayoutInflaterImpl : ILayoutInflater() {
processor: XmlProcessor,
parent: ViewGroup,
module: AndroidModule,
): IView? {
): List<IView> {
return doInflate(processor, module) { wrap(parent) }
}
protected open fun doInflate(
processor: XmlProcessor,
module: AndroidModule,
parent: () -> IViewGroup,
): List<IView> {
// TODO(itsaky) : Add test for multiple view as root layout
// The inflater should fail in such cases
val (file, node) =
@@ -132,11 +120,11 @@ open class LayoutInflaterImpl : ILayoutInflater() {
.forEach { IDTable.set(currentLayoutFile.resName, it.name.entry!!, View.generateViewId()) }
val element = node.element
val view = onCreateView(element, wrap(parent), module)
val views = onCreateView(element, parent(), module)
this._currentLayoutFile = null
return view
return views
}
protected open fun onCreateView(
@@ -146,16 +134,15 @@ open class LayoutInflaterImpl : ILayoutInflater() {
widgets: WidgetTable =
module.getWidgetTable()
?: throw IllegalStateException("No widget table found for module $module"),
): IView {
): List<IView> {
val parentView = parent.view as ViewGroup
if ("include" == element.name) {
return onCreateIncludedView(element, parentView, module, widgets)
if ("merge" == element.name) {
return onCreateMergedView(element, parent, module, widgets)
}
if ("merge" == element.name) {
return onCreateMergedView(element, parentView, module, widgets)
if ("include" == element.name) {
return onCreateIncludedView(element, parent)
}
val widget =
@@ -171,32 +158,8 @@ open class LayoutInflaterImpl : ILayoutInflater() {
})
as ViewImpl
if (element.namespaceDeclarationCount > 0) {
for (xmlNamespace in element.namespaceDeclarationList) {
view.namespaceDecls[xmlNamespace.uri] = NamespaceImpl(xmlNamespace.prefix, xmlNamespace.uri)
}
}
val adapter =
AttributeAdapterIndex.getAdapter(view.name)
?: throw InflateException("No attribute adapter found for view ${view.name}")
view.view.layoutParams = generateLayoutParams(parentView)
parent.addChild(view)
adapter.applyBasic(view)
if (element.attributeCount > 0) {
for (xmlAttribute in element.attributeList) {
val namespace =
view.findNamespaceByUri(xmlAttribute.namespaceUri)
?: throw InflateException("Unknown namespace : ${xmlAttribute.namespaceUri}")
val attr =
AttributeImpl(namespace = namespace, name = xmlAttribute.name, value = xmlAttribute.value)
view.addAttribute(attr)
inflationEventListener?.onEvent(OnApplyAttributeEvent(view, attr))
}
}
addNamespaceDecls(element, view)
applyAttributes(element, view, parent)
if (element.childCount > 0 && view is IViewGroup) {
for (child in element.childList) {
@@ -206,28 +169,94 @@ open class LayoutInflaterImpl : ILayoutInflater() {
onCreateView(element = child.element, parent = view, module = module, widgets = widgets)
}
}
inflationEventListener?.onEvent(OnInflateViewEvent(view))
return view
return listOf(view)
}
@JvmOverloads
protected open fun applyAttributes(
element: XmlElement,
view: ViewImpl,
parent: IViewGroup,
attachToParent: Boolean = true,
checkAttr: (XmlAttribute) -> Boolean = { true }
) {
val parentView = parent.view as ViewGroup
val adapter =
AttributeAdapterIndex.getAdapter(view.name)
?: throw InflateException("No attribute adapter found for view ${view.name}")
view.view.layoutParams = generateLayoutParams(parentView)
if (attachToParent) {
parent.addChild(view)
}
adapter.applyBasic(view)
if (element.attributeCount > 0) {
for (xmlAttribute in element.attributeList) {
if (!checkAttr(xmlAttribute)) {
continue
}
val namespace =
view.findNamespaceByUri(xmlAttribute.namespaceUri)
?: throw InflateException("Unknown namespace : ${xmlAttribute.namespaceUri}")
val attr =
AttributeImpl(namespace = namespace, name = xmlAttribute.name, value = xmlAttribute.value)
view.addAttribute(attr)
inflationEventListener?.onEvent(OnApplyAttributeEvent(view, attr))
}
}
}
private fun onCreateMergedView(
element: XmlElement,
parent: ViewGroup,
parent: IViewGroup,
module: AndroidModule,
widgets: WidgetTable,
): IView {
TODO("Not yet implemented")
): List<IView> {
val views = mutableListOf<IView>()
for (xmlNode in element.childList) {
if (xmlNode.nodeCase == ELEMENT) {
views.addAll(onCreateView(xmlNode.element, parent, module, widgets))
}
}
addNamespaceDecls(element, parent as ViewImpl)
return views
}
private fun onCreateIncludedView(
element: XmlElement,
parent: ViewGroup,
module: AndroidModule,
widgets: WidgetTable,
): IView {
TODO("Not yet implemented")
private fun onCreateIncludedView(element: XmlElement, parent: IViewGroup): List<IView> {
val layout =
element.attributeList.find { it.namespaceUri.isNullOrBlank() && it.name == "layout" }
?: throw InflateException("<include> tag must have 'layout' attribute")
val file = parseLayoutReference(layout.value)
if (file == null || !file.exists() || !file.isFile || file.extension != EXT_XML) {
throw InflateException("Invalid layout file reference; '${layout.value}'")
}
val (processor, module) = processXmlFile(file)
val inflated = doInflate(processor, module) { parent }
if (inflated.isEmpty() || inflated.size > 1) {
// probably a merged view or no views at all
// no need to apply attributes
return inflated
}
val view = inflated[0] as ViewImpl
addNamespaceDecls(element = element, view = view)
// The inflated <include> view is already attached to parent
// so we don't need to do it here
applyAttributes(element = element, view = view, parent = parent, attachToParent = false) {
!(it.namespaceUri.isNullOrBlank() && it.name == "layout")
}
return inflated
}
protected open fun onCreatePlatformView(
@@ -272,6 +301,40 @@ open class LayoutInflaterImpl : ILayoutInflater() {
}
}
protected open fun addNamespaceDecls(element: XmlElement, view: ViewImpl) {
if (element.namespaceDeclarationCount > 0) {
for (xmlNamespace in element.namespaceDeclarationList) {
view.namespaceDecls[xmlNamespace.uri] = NamespaceImpl(xmlNamespace.prefix, xmlNamespace.uri)
}
}
}
protected open fun processXmlFile(file: File): Pair<XmlProcessor, AndroidModule> {
val pathData = extractPathData(file)
if (pathData.type != LAYOUT) {
throw InflateException("File is not a layout file.")
}
if (ProjectManager.rootProject == null) {
throw InflateException("Project is not initialized!")
}
val module =
ProjectManager.findModuleForFile(file) as? AndroidModule
?: throw InflateException("Cannot find module for given file. Is the project initialized?")
val resFile =
ResourceFile(
ResourceName(module.packageName, pathData.type!!, pathData.name),
pathData.config,
pathData.source,
ProtoXml
)
val processor = XmlProcessor(pathData.source, BlameLogger(IDELogger))
processor.process(resFile, file.inputStream())
return processor to module
}
private fun createViewInstance(widget: Widget, parent: ViewGroup): View {
val klass = javaClass.classLoader!!.loadClass(widget.qualifiedName)
val constructor = klass.getConstructor(Context::class.java)
@@ -18,10 +18,8 @@
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: LayoutFile, name: String, view: ViewGroup) :
ViewImpl(file = file, name = name, view = view), IViewGroup {
@@ -68,4 +66,11 @@ open class ViewGroupImpl(file: LayoutFile, name: String, view: ViewGroup) :
addChild(index, view)
return existing
}
override fun printHierarchy(builder: StringBuilder, indent: Int) {
super.printHierarchy(builder, indent)
for (child in children) {
(child as ViewImpl).printHierarchy(builder, indent + 1)
}
}
}
@@ -59,4 +59,14 @@ constructor(
internal fun findNamespaceByUri(uri: String): INamespace? {
return this.namespaceDecls[uri] ?: (parent as? ViewImpl)?.findNamespaceByUri(uri)
}
internal open fun printHierarchy() : String {
return StringBuilder().apply { printHierarchy(this, 0) }.toString()
}
internal open fun printHierarchy(builder: StringBuilder, indent: Int) {
builder.append(" ".repeat(indent * 4))
builder.append(name)
builder.append("\n")
}
}
@@ -39,11 +39,6 @@ import com.itsaky.androidide.inflater.IView
import com.itsaky.androidide.inflater.internal.AttributeImpl
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.startParse
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
/**
* Attribute adapter for [View].
@@ -275,10 +270,6 @@ open class ViewAttrAdapter : IAttributeAdapter() {
@Suppress("UNCHECKED_CAST")
return (view.view as T).let {
val file = (view as ViewImpl).file
val module =
ProjectManager.findModuleForFile(file.file) as? AndroidModule
?: throw IllegalStateException("Cannot find module for file: $file")
startParse(module)
val applied =
it.block(
file,
@@ -288,7 +279,6 @@ open class ViewAttrAdapter : IAttributeAdapter() {
attribute.name,
attribute.value
)
endParse()
return@let applied
}
}
@@ -312,7 +302,7 @@ open class ViewAttrAdapter : IAttributeAdapter() {
protected open fun parsePorterDuffMode(mode: String): PorterDuff.Mode {
return com.itsaky.androidide.inflater.internal.utils.parsePorterDuffMode(mode)
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
protected open fun isApi29(): Boolean {
return Build.VERSION.SDK_INT >= 29
@@ -24,6 +24,7 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.TypedValue.complexToDimension
import android.view.Gravity
import android.view.InflateException
import android.view.ViewGroup.LayoutParams
import androidx.core.text.isDigitsOnly
import com.android.SdkConstants.EXT_XML
@@ -35,6 +36,7 @@ import com.android.aaptcompiler.AaptResourceType.COLOR
import com.android.aaptcompiler.AaptResourceType.DIMEN
import com.android.aaptcompiler.AaptResourceType.DRAWABLE
import com.android.aaptcompiler.AaptResourceType.INTEGER
import com.android.aaptcompiler.AaptResourceType.LAYOUT
import com.android.aaptcompiler.AaptResourceType.STRING
import com.android.aaptcompiler.ArrayResource
import com.android.aaptcompiler.AttributeResource
@@ -200,6 +202,24 @@ fun parseDrawable(context: Context, value: String, def: Drawable = unknownDrawab
return def
}
fun parseLayoutReference(value: String): File? {
if (value[0] != '@') {
throw InflateException("Value must be a reference to a layout file")
}
val layoutResolver: (Value?) -> File? =
fun(it): File? {
return if (it is FileReference) {
File(it.source.path)
} else null
}
val type = parseResourceReference(value)?.second ?: return null
if (type != LAYOUT) {
throwInvalidResType(LAYOUT, value)
}
return parseReference(value, type, null, layoutResolver)
}
@JvmOverloads
fun parseColorDrawable(context: Context, value: String, def: Int = Color.TRANSPARENT): Drawable {
return newColorDrawable(parseColor(context, value, def))
@@ -26,11 +26,6 @@ val module: AndroidModule
get() =
currentModule ?: throw IllegalStateException("You must call startParse(AndroidModule) first")
fun startParseIfNeeded(file: File) {
if (currentModule != null) return
(ProjectManager.findModuleForFile(file) as? AndroidModule)?.also { startParse(it) }
}
fun startParse(m: AndroidModule) {
currentModule = m
}
@@ -44,8 +44,9 @@ class LayoutInflaterTest {
val inflater = ILayoutInflater.newInflater()
val inflated = inflater.inflate(file, parent)
inflated.apply {
assertThat(this).isNotNull()
assertThat(this!!.view).isInstanceOf(View::class.java)
assertThat(this).isNotEmpty()
assertThat(this).hasSize(1)
forEach { assertThat(it.view).isInstanceOf(View::class.java) }
}
}
}
@@ -62,10 +63,9 @@ class LayoutInflaterTest {
parent.removeAllViews()
module.createLayoutFile(view.simpleName!!) { file ->
file.writeText(viewDeclTemplate(view.simpleName!!))
val inflated = inflater.inflate(file, parent)
assertThat(inflated!!.view).isInstanceOf(view.java)
assertThat(inflated.view.id)
.isEqualTo(IDTable.get(view.simpleName!!, "template_view"))
val inflated = inflater.inflate(file, parent)[0]
assertThat(inflated.view).isInstanceOf(view.java)
assertThat(inflated.view.id).isEqualTo(IDTable.get(view.simpleName!!, "template_view"))
(inflated as ViewImpl).attributes.apply {
assertThat(this)
@@ -81,6 +81,45 @@ class LayoutInflaterTest {
}
}
@Test
fun `verify included view hierarchy`() {
inflaterTest { module ->
requiresActivity { activity ->
val parent = LinearLayout(activity)
val inflater = ILayoutInflater.newInflater()
val inflated = inflater.inflate(layoutFile("include"), parent)
assertThat(inflated).hasSize(1)
val view = inflated[0] as ViewImpl
assertThat(view.printHierarchy())
.isEqualTo(
"android.widget.LinearLayout\n" +
" android.widget.RelativeLayout\n" +
" android.widget.TextView\n" +
" android.widget.TextView\n"
)
}
}
}
@Test
fun `verify merged view hierarchy`() {
inflaterTest { module ->
requiresActivity { activity ->
val parent = LinearLayout(activity)
val inflater = ILayoutInflater.newInflater()
val inflated = inflater.inflate(layoutFile("merge"), parent)
assertThat(inflated).hasSize(1)
val view = inflated[0] as ViewImpl
assertThat(view.printHierarchy())
.isEqualTo(
"android.widget.LinearLayout\n android.widget.TextView\n android.widget.TextView\n"
)
}
}
}
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")
@@ -19,13 +19,9 @@ package com.itsaky.androidide.inflater
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ClipDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.ShapeDrawable
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.inflater.internal.utils.endParse
import com.itsaky.androidide.inflater.internal.utils.parseBoolean
import com.itsaky.androidide.inflater.internal.utils.parseColor
import com.itsaky.androidide.inflater.internal.utils.parseDimension
@@ -34,7 +30,6 @@ import com.itsaky.androidide.inflater.internal.utils.parseInteger
import com.itsaky.androidide.inflater.internal.utils.parseIntegerArray
import com.itsaky.androidide.inflater.internal.utils.parseString
import com.itsaky.androidide.inflater.internal.utils.parseStringArray
import com.itsaky.androidide.inflater.internal.utils.startParse
import kotlin.math.roundToInt
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,7 +42,6 @@ class ValueParsersTest {
fun `hardcoded dimension parser test`() {
inflaterTest { module ->
requiresActivity { activity ->
startParse(module)
// Hardcoded dimensions
val units = arrayOf("dp", "sp", "pt", "px", "in", "mm")
@@ -84,7 +78,6 @@ class ValueParsersTest {
assertThat(parseDimension(activity, "@dimen/test_dimen_1pt", def = 0f)).isEqualTo(1)
assertThat(parseDimension(activity, "@dimen/test_dimen_1pt_ref", def = 0f)).isEqualTo(1)
assertThat(parseDimension(activity, "@dimen/test_dimen_0pt_ref", def = 0f)).isEqualTo(0)
endParse()
}
}
}
@@ -93,7 +86,6 @@ class ValueParsersTest {
fun `drawable parse test`() {
inflaterTest { module ->
requiresActivity { activity ->
startParse(module)
parseDrawable(activity, "#ff0000").apply {
assertThat(this).isNotNull()
assertThat(this).isInstanceOf(ColorDrawable::class.java)
@@ -107,7 +99,6 @@ class ValueParsersTest {
assertThat(this).isNotNull()
assertThat(this).isInstanceOf(GradientDrawable::class.java)
}
endParse()
}
}
}
@@ -116,7 +107,6 @@ class ValueParsersTest {
fun `boolean parser test`() {
inflaterTest { module ->
requiresActivity {
startParse(module)
assertThat(parseBoolean("true")).isTrue()
assertThat(parseBoolean("false", def = true)).isFalse()
assertThat(parseBoolean("@android:bool/resolver_landscape_phone")).isTrue()
@@ -125,7 +115,6 @@ class ValueParsersTest {
assertThat(parseBoolean("@bool/test_bool_false", def = true)).isFalse()
assertThat(parseBoolean("@bool/test_bool_true_ref")).isTrue()
assertThat(parseBoolean("@bool/test_bool_false_ref", def = true)).isFalse()
endParse()
}
}
}
@@ -134,7 +123,6 @@ class ValueParsersTest {
fun `integer parser test`() {
inflaterTest { module ->
requiresActivity {
startParse(module)
assertThat(parseInteger("0", def = 1)).isEqualTo(0)
assertThat(parseInteger("10")).isEqualTo(10)
assertThat(parseInteger("110")).isEqualTo(110)
@@ -147,7 +135,6 @@ class ValueParsersTest {
assertThat(parseInteger("@integer/test_integer_1_ref")).isEqualTo(1)
assertThat(parseInteger("@integer/test_integer_0", def = 1)).isEqualTo(0)
assertThat(parseInteger("@integer/test_integer_0_ref", def = 1)).isEqualTo(0)
endParse()
}
}
}
@@ -155,7 +142,6 @@ class ValueParsersTest {
@Test
fun `string parser test`() {
inflaterTest {
startParse(it)
assertThat(parseString("Hello World!")).isEqualTo("Hello World!")
assertThat(parseString("@android:string/ok")).isEqualTo("OK")
assertThat(parseString("@android:string/cancel")).isEqualTo("Cancel")
@@ -163,34 +149,29 @@ class ValueParsersTest {
assertThat(parseString("@string/test_string_ref")).isEqualTo("I love Android!")
assertThat(parseString("@string/test_string_styled")).isEqualTo("I love Android!")
assertThat(parseString("@string/test_string_styled_ref")).isEqualTo("I love Android!")
endParse()
}
}
@Test
fun `string array parser test`() {
inflaterTest {
startParse(it)
assertThat(parseStringArray("@android:array/phoneTypes"))
.isEqualTo(
arrayOf("Home", "Mobile", "Work", "Work Fax", "Home Fax", "Pager", "Other", "Custom")
)
assertThat(parseStringArray("@array/test_string_array"))
.isEqualTo(arrayOf("I", "love", "Android"))
endParse()
}
}
@Test
fun `integer array parser test`() {
inflaterTest {
startParse(it)
assertThat(
parseIntegerArray("@android:array/config_defaultImperceptibleKillingExemptionProcStates")
)
.isEqualTo(intArrayOf(0, 1, 2, 4, 12))
assertThat(parseIntegerArray("@array/test_integer_array")).isEqualTo(intArrayOf(2, 4, 8))
endParse()
}
}
@@ -198,7 +179,6 @@ class ValueParsersTest {
fun `color parser test`() {
inflaterTest {
requiresActivity { activity ->
startParse(it)
assertThat(parseColor(activity, "@android:color/red")).isEqualTo(Color.RED)
assertThat(parseColor(activity, "@android:color/black")).isEqualTo(Color.BLACK)
assertThat(parseColor(activity, "@android:color/white")).isEqualTo(Color.WHITE)
@@ -209,7 +189,6 @@ class ValueParsersTest {
// TODO Implement color state list parser
assertThat(parseColor(activity, "@color/test_selector")).isEqualTo(Color.TRANSPARENT)
endParse()
}
}
}
@@ -18,6 +18,8 @@
package com.itsaky.androidide.inflater
import androidx.appcompat.app.AppCompatActivity
import com.itsaky.androidide.inflater.internal.utils.endParse
import com.itsaky.androidide.inflater.internal.utils.startParse
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
@@ -51,7 +53,9 @@ object XmlInflaterTest {
fun inflaterTest(block: (AndroidModule) -> Unit) {
XmlInflaterTest.initIfNeeded()
startParse(ProjectManager.app!!)
block(ProjectManager.app!!)
endParse()
}
fun requiresActivity(block: (AppCompatActivity) -> Unit) {