Added basic value parsers

This commit is contained in:
Akash Yadav
2022-11-04 19:56:06 +05:30
parent 070fa83601
commit ceceab12d5
15 changed files with 572 additions and 51 deletions
@@ -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()
@@ -27,4 +27,4 @@ import kotlin.reflect.KClass
*/
@Target(CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AttributeAdapter(val forClass: KClass<out View>)
annotation class AttributeAdapter(val forView: KClass<out View>)
@@ -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<ResourceTable> =
mutableListOf<ResourceTable>().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<ResourceTable> {
return mutableSetOf<ResourceTable>().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
@@ -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/>.
*/
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|\\.)*)") }
@@ -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) ?: ""
Binary file not shown.
@@ -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
}
@@ -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 {
@@ -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<IView>()
override val childCount: Int
@@ -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(),
@@ -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>(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 <T : View> 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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String?, String, String> {
// 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<String?, String, String>(null, matcher.group(1)!!, matcher.group(2)!!)
}
return Triple<String?, String, String>(null, "", "")
}
@@ -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<AppCompatActivity>
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 {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}