fix: cut down memory usage in ApiVersionsRegistry

This commit adds the following improvements to the ApiVersionsRegistry implementation :

- Use Java StAX parser for memory-efficient XML processing
- Reduce object allocations for representing API version info by reusing ApiVersion instances which represent the same API versions.
- Trim method return types in method signature strings to reduce memory usage by a small amount
- A single model class is now used to represent the API information. Earlier, we had different classes to represent API info for classes, fields and metnods.
This commit is contained in:
Akash Yadav
2024-06-24 12:27:46 +05:30
parent c3e13305d7
commit 52a6117451
32 changed files with 342 additions and 737 deletions
@@ -22,7 +22,7 @@ import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
@@ -32,7 +32,7 @@ import com.itsaky.androidide.activities.editor.EditorActivityKt
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityMainBinding
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.utils.DialogUtils
@@ -84,7 +84,7 @@ import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SearchResult
import com.itsaky.androidide.preferences.internal.BuildPreferences
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.tasks.cancelIfActive
import com.itsaky.androidide.ui.CodeEditorView
import com.itsaky.androidide.ui.ContentTranslatingDrawerLayout
@@ -51,7 +51,7 @@ import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.ui.CodeEditorView
import com.itsaky.androidide.utils.DialogUtils.newYesNoDialog
@@ -39,7 +39,7 @@ import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.IDELanguageClientImpl
import com.itsaky.androidide.lsp.java.utils.CancelChecker
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.projects.api.GradleProject
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.services.builder.GradleBuildService
@@ -28,7 +28,7 @@ import com.itsaky.androidide.eventbus.events.editor.OnPauseEvent
import com.itsaky.androidide.eventbus.events.editor.OnResumeEvent
import com.itsaky.androidide.eventbus.events.editor.OnStartEvent
import com.itsaky.androidide.eventbus.events.editor.OnStopEvent
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.projects.util.BootClasspathProvider
import com.itsaky.androidide.utils.EditorActivityActions
import com.itsaky.androidide.utils.EditorSidebarActions
@@ -34,7 +34,7 @@ import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.managers.ToolsManager
import com.itsaky.androidide.preferences.internal.BuildPreferences
import com.itsaky.androidide.preferences.internal.DevOpsPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.services.ToolingServerNotStartedException
@@ -1,205 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.indexing.core.internal.platform
import androidx.collection.mutableIntObjectMapOf
import com.itsaky.androidide.indexing.core.platform.ApiVersion
import jaxp.xml.namespace.QName
import jaxp.xml.stream.XMLInputFactory
import jaxp.xml.stream.events.Attribute
import jaxp.xml.stream.events.EndElement
import jaxp.xml.stream.events.StartElement
import org.slf4j.LoggerFactory
import java.io.InputStream
/**
* Parser for parsing `api-versions.xml` file from the Android SDK and building [ApiVersion] models.
*
* @author Akash Yadav
*/
open class ApiVersionsParser {
// A cache to store the ApiVersion instances with different values
// For example, if two classes have the same value for `since`,
// 'removed' and 'deprecated', then the same ApiVersion instance
// will be used for the two classes.
// The keys in this map are the packed (bitwise ORed) values of the three version integers.
// The first 8 bits are 0, the next 8 bits represent 'since', the next 8 bits represent 'deprecated'
// and the last 8 bits represent 'removed'.
private val apiInfosCache = mutableIntObjectMapOf<ApiVersion>()
private var apiVersion: Int? = null
private var currentClass: String? = null
companion object {
private val log = LoggerFactory.getLogger(ApiVersionsParser::class.java)
}
fun parse(apiVersionsXml: InputStream) {
if (apiVersionsXml.available() <= 0) {
log.warn("api-versions.xml InputStream is empty")
return
}
val inputFactory = XMLInputFactory.newInstance()
val reader = inputFactory.createXMLEventReader(apiVersionsXml)
while (reader.hasNext()) {
val event = reader.nextEvent()
if (event.isStartElement) {
consumeStartElement(event.asStartElement())
continue
}
if (event.isEndElement) {
consumeEndElement(event.asEndElement())
}
}
onFinishParse()
}
/**
* Called when the parser is done parsing the `api-versions.xml` file.
*/
protected open fun onFinishParse() {}
private fun consumeStartElement(event: StartElement) {
when (event.name.localPart) {
"api" -> apiVersion = event.getAttributeByName(QName("version")).value.toInt()
"class" -> consumeClass(event)
"field" -> consumeMember(event, "field")
"method" -> consumeMember(event, "method")
}
}
private fun consumeClass(event: StartElement) {
checkNotNull(apiVersion) {
"<class> element must be inside <api> element"
}
check(currentClass == null) {
"<class> elements cannot be nested"
}
val (name, versions) = event.parseAttrs()
check(!isDuplicateClass(name)) {
"Duplicate class entry: $name"
}
val apiInfo = apiInfosCache.getOrPut(versions) {
createApiInfo(versions)
}
consumeClassVersionInfo(name, apiInfo)
this.currentClass = name
}
/**
* Check if the given class name is a duplicate.
*/
protected open fun isDuplicateClass(name: String): Boolean {
return false
}
/**
* Consume [ApiVersion] info of a class.
*
* @param name The fully qualified name of the class in its internal form.
* @param apiVersion The [ApiVersion] info of the class.
*/
protected open fun consumeClassVersionInfo(name: String, apiVersion: ApiVersion) {}
private fun consumeMember(event: StartElement, memberType: String) {
val (name, versions) = event.parseAttrs()
val currentClass = checkNotNull(currentClass) {
"<${memberType}> element must be inside <class> element"
}
check(!isDuplicateMember(currentClass, name)) {
"Duplicate $memberType entry in class $currentClass: $name"
}
consumeMemberVersionInfo(currentClass, name, memberType, createApiInfo(versions))
}
/**
* Check if the given member of the given class is a duplicate.
*/
protected open fun isDuplicateMember(className: String, memberName: String): Boolean {
return false
}
/**
* Consume [ApiVersion] info for a member of a class.
*
* @param className The name of the class which declares this member.
* @param member The identifier of the member. This is the name of the field and the method signature for methods.
* @param memberType The type of member. This may be "field" or "method".
* @param apiVersion The [ApiVersion] info.
*/
protected open fun consumeMemberVersionInfo(
className: String,
member: String,
memberType: String,
apiVersion: ApiVersion
) {
}
private fun consumeEndElement(element: EndElement) {
when (element.name.localPart) {
"api" -> apiVersion = null
"class" -> currentClass = null
}
}
private fun StartElement.parseAttrs(): Pair<String, Int> {
var name: String? = null
var since = 1
var deprecated = 0
var removed = 0
attributes.forEach { attribute ->
attribute as Attribute
when (attribute.name.localPart) {
"name" -> name = attribute.value
"since" -> since = attribute.value.toInt()
"deprecated" -> deprecated = attribute.value.toInt()
"removed" -> removed = attribute.value.toInt()
}
}
checkNotNull(name) {
"Missing name attribute"
}
// Android API versions would not exceed 255, right?
check(since in 1..255 && deprecated in 0..255 && removed in 0..255) {
"Invalid version: $since, $deprecated, $removed"
}
val version = (since shl 16) or (deprecated shl 8) or removed
return name!! to version
}
private fun createApiInfo(versions: Int): ApiVersion {
val since = (versions shr 16) and 0x000000FF
val deprecated = (versions shr 8) and 0x000000FF
val removed = versions and 0x000000FF
return ApiVersion.newInstance(since = since, deprecatedIn = deprecated, removedIn = removed)
}
}
@@ -1,122 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.indexing.core.platform
import com.google.common.base.Objects
import com.itsaky.androidide.indexing.IIndexable
import com.itsaky.androidide.models.ICloneable
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import io.realm.annotations.RealmField
import io.realm.annotations.Required
/**
* @property since The API in which the symbol was added.
* @property deprecatedIn The API in which the symbol was deprecated.
* @property removedIn The API in which the symbol was removed.
* @author Akash Yadav
*/
@RealmClass
open class ApiVersion : IIndexable, ICloneable {
@Required
@PrimaryKey
@RealmField("id")
var id: Int? = null
@RealmField("since")
var since: Int = 1
private set
@RealmField("deprecatedIn")
var deprecatedIn: Int = 0
private set
@RealmField("removedIn")
var removedIn: Int = 0
private set
private fun computeId() {
this.id = Objects.hashCode(this.since, this.removedIn, this.deprecatedIn)
}
/**
* Update the API information from the given [ApiVersion].
*/
fun update(apiVersion: ApiVersion) = apply {
this.since = apiVersion.since
this.deprecatedIn = apiVersion.deprecatedIn
this.removedIn = apiVersion.removedIn
this.computeId()
}
/**
* Update the API information with the given parameters.
*/
fun update(
since: Int = this.since,
deprecatedIn: Int = this.deprecatedIn,
removedIn: Int = this.removedIn
) = apply {
this.since = since
this.deprecatedIn = deprecatedIn
this.removedIn = removedIn
this.computeId()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ApiVersion) return false
if (id != other.id) return false
if (since != other.since) return false
if (deprecatedIn != other.deprecatedIn) return false
if (removedIn != other.removedIn) return false
return true
}
override fun hashCode(): Int {
var result = id ?: 0
result = 31 * result + since
result = 31 * result + deprecatedIn
result = 31 * result + removedIn
return result
}
override fun toString(): String {
return "ApiVersion(id=$id, since=$since, deprecatedIn=$deprecatedIn, removedIn=$removedIn)"
}
override fun clone(): ApiVersion {
return newInstance(since, deprecatedIn, removedIn)
}
companion object {
@JvmStatic
fun newInstance(since: Int, deprecatedIn: Int, removedIn: Int): ApiVersion {
return ApiVersion().apply {
this.since = since
this.deprecatedIn = deprecatedIn
this.removedIn = removedIn
this.computeId()
}
}
}
}
@@ -1,345 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.indexing.core.platform
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.testing.common.utils.findAndroidHome
import jaxp.xml.stream.XMLStreamException
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.ByteArrayInputStream
import java.io.File
/**
* @author Akash Yadav
*/
@RunWith(RobolectricTestRunner::class)
class ApiVersionParserTest {
val apiVersionXml by lazy {
val platformsDir = File(findAndroidHome(), "platforms")
val platformDir = platformsDir.listFiles()!!.asSequence().sortedByDescending {
// android-30, android-31, android-32, android-33, etc
it.name.substring("android-".length).toIntOrNull() ?: 0
}.first()
platformDir.resolve("data/api-versions.xml")
}
private lateinit var parser: CollectingApiVersionsParser
@Before
fun setup() {
parser = CollectingApiVersionsParser()
}
@Test
fun `test basic parse`() {
parser.parse(apiVersionXml.inputStream().buffered())
val manifestInfo = parser.getClassInfo("android/Manifest")
assertThat(manifestInfo).isNotNull()
assertThat(manifestInfo?.since).isEqualTo(1)
assertThat(manifestInfo?.deprecatedIn).isEqualTo(0)
assertThat(manifestInfo?.removedIn).isEqualTo(0)
}
@Test
fun testSingleClassParsing() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
assertThat(classInfo.deprecatedIn).isEqualTo(0)
assertThat(classInfo.removedIn).isEqualTo(0)
}
@Test
fun testSingleClassWithMembersParsing() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0">
<field name="fieldA" since="2" deprecated="0" removed="0"/>
<method name="methodA" since="3" deprecated="0" removed="0"/>
</class>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
val fieldInfo = parser.getMemberInfo("com.example.ClassA", "fieldA")
assertThat(fieldInfo).isNotNull()
assertThat(fieldInfo!!.since).isEqualTo(2)
val methodInfo = parser.getMemberInfo("com.example.ClassA", "methodA")
assertThat(methodInfo).isNotNull()
assertThat(methodInfo!!.since).isEqualTo(3)
}
@Test
fun testMultipleClassesParsing() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
<class name="com.example.ClassB" since="2" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
parser.parse(inputStream)
val classAInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classAInfo).isNotNull()
assertThat(classAInfo!!.since).isEqualTo(1)
val classBInfo = parser.getClassInfo("com.example.ClassB")
assertThat(classBInfo).isNotNull()
assertThat(classBInfo!!.since).isEqualTo(2)
}
@Test
fun testClassWithDeprecatedAndRemovedAttributes() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="30" removed="34"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
assertThat(classInfo.deprecatedIn).isEqualTo(30)
assertThat(classInfo.removedIn).isEqualTo(34)
}
@Test
fun testClassWithInvalidAttributes() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="256" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Invalid version")
}
}
@Test
fun testMissingNameAttribute() {
val xml = """
<api version="34">
<class since="1" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Missing name attribute")
}
}
@Test
fun testDuplicateClassEntry() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
<class name="com.example.ClassA" since="2" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Duplicate class entry")
}
}
@Test
fun testDuplicateMemberEntry() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0">
<field name="fieldA" since="2" deprecated="0" removed="0"/>
<field name="fieldA" since="3" deprecated="0" removed="0"/>
</class>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
try {
parser.parse(inputStream)
assertThat(true).isFalse() // Should not reach here
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().contains("Duplicate field entry in class com.example.ClassA")
}
}
@Test
fun testMultipleApiElements() {
val xml = """
<api version="33">
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
</api>
<api version="34">
<class name="com.example.ClassB" since="2" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
val parser = CollectingApiVersionsParser()
assertThrows(
"The markup in the document following the root element must be well-formed.",
XMLStreamException::class.java
) {
parser.parse(inputStream)
}
}
@Test
fun testNestedClassElements() {
val xml = """
<api version="34">
<class name="com.example.OuterClass" since="1" deprecated="0" removed="0">
<class name="com.example.OuterClass.InnerClass" since="2" deprecated="0" removed="0"/>
</class>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
assertThrows("<class> elements cannot be nested", IllegalStateException::class.java) {
parser.parse(inputStream)
}
}
@Test
fun testBoundaryAttributeValues() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="255" removed="0"/>
<class name="com.example.ClassB" since="1" deprecated="0" removed="255"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
parser.parse(inputStream)
val classAInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classAInfo).isNotNull()
assertThat(classAInfo!!.deprecatedIn).isEqualTo(255)
val classBInfo = parser.getClassInfo("com.example.ClassB")
assertThat(classBInfo).isNotNull()
assertThat(classBInfo!!.removedIn).isEqualTo(255)
}
@Test
fun testOutOfBoundaryAttributeValues() {
val xml = """
<api version="34">
<class name="com.example.ClassA" since="1" deprecated="256" removed="0"/>
<class name="com.example.ClassB" since="1" deprecated="0" removed="256"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
assertThrows("Invalid version: 1, 256, 0", IllegalStateException::class.java) {
parser.parse(inputStream)
}
}
@Test
fun testUnexpectedElements() {
val xml = """
<api version="34">
<unexpectedElement name="unexpected" since="1" deprecated="0" removed="0"/>
<class name="com.example.ClassA" since="1" deprecated="0" removed="0"/>
</api>
""".trimIndent()
val inputStream = ByteArrayInputStream(xml.toByteArray())
parser.parse(inputStream)
val classInfo = parser.getClassInfo("com.example.ClassA")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo(1)
}
@Test
fun testEmptyXmlFile() {
val xml = ""
val inputStream = ByteArrayInputStream(xml.toByteArray())
parser.parse(inputStream)
}
@Test
fun testLargeInputFile() {
val xmlBuilder = StringBuilder()
xmlBuilder.append("<api version=\"34\">")
for (i in 1..1000) {
xmlBuilder.append("<class name=\"com.example.Class$i\" since=\"${(i % 254) + 1}\" />")
}
xmlBuilder.append("</api>")
val inputStream = ByteArrayInputStream(xmlBuilder.toString().toByteArray())
val parser = CollectingApiVersionsParser()
parser.parse(inputStream)
for (i in 1..1000) {
val classInfo = parser.getClassInfo("com.example.Class$i")
assertThat(classInfo).isNotNull()
assertThat(classInfo!!.since).isEqualTo((i % 254) + 1)
assertThat(classInfo.deprecatedIn).isEqualTo(0)
assertThat(classInfo.removedIn).isEqualTo(0)
}
}
}
@@ -1,79 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.indexing.core.platform
import com.itsaky.androidide.indexing.platform.ApiVersionsParser
/**
* @author Akash Yadav
*/
class CollectingApiVersionsParser : com.itsaky.androidide.indexing.platform.ApiVersionsParser() {
private val apiInfo = mutableMapOf<String, Pair<ApiVersion, MutableMap<String, ApiVersion>>>()
override fun isDuplicateClass(name: String): Boolean {
return apiInfo.containsKey(name)
}
override fun consumeClassVersionInfo(name: String, apiVersion: ApiVersion) {
this.apiInfo[name] = apiVersion to HashMap()
}
override fun isDuplicateMember(className: String, memberName: String): Boolean {
return apiInfo[className]?.second?.containsKey(memberName) == true
}
override fun consumeMemberVersionInfo(
className: String,
member: String,
memberType: String,
apiVersion: ApiVersion
) {
val (_, members) = apiInfo[className]!!
val existing = members.put(member, apiVersion)
check(existing == null) {
"Duplicate $memberType entry in class $className: $member"
}
}
/**
* Returns [ApiVersion] for the given class.
*/
fun getClassInfo(className: String): ApiVersion? {
return apiInfo[className]?.first
}
/**
* Returns [ApiVersion] for the given class and member.
*/
fun getMemberInfo(className: String, memberName: String): ApiVersion? {
return apiInfo[className]?.second?.get(memberName)
}
/**
* Removes and returns the [ApiVersion] for the given class and member. This also removes the [ApiVersion]
* for the class if the all the members of the class have been removed.
*/
fun removeMemberInfo(className: String, memberName: String): ApiVersion? {
return apiInfo[className]?.second?.remove(memberName).also {
if (apiInfo[className]?.second?.isEmpty() == true) {
apiInfo.remove(className)
}
}
}
}
@@ -23,9 +23,12 @@ interface ICompletionData
/**
* Information about a class-related completion item.
*
* @property className The fully qualified name of the class.
* @property className The fully qualified name of the class. Example: `pck.outer.inner`.
* @property flatName The flat name of the class. Example: `pck.outer$inner`.
* @property isCompleteData Whether the data provided by this [ClassCompletionData] is complete.
* @property isNested Whether the given class is a nested class or not.
* @property topLevelClass If [isNested] is true, then this must be set to the fully qualified name
* of the top-level class.
* @property simpleName The simple name of the class.
* @property nameWithoutTopLevel The name of this class without the fully qualified name of its top
* level class. For example, the value of this property for class name
@@ -33,20 +36,20 @@ interface ICompletionData
*/
data class ClassCompletionData
@JvmOverloads
constructor(val className: String, val isNested: Boolean = false, val topLevelClass: String = "") :
ICompletionData {
val simpleName: String
get() {
return className.substringAfterLast(delimiter = '.')
}
constructor(
val className: String,
val isCompleteData: Boolean = false,
val flatName: String = className,
val simpleName: String = className.substringAfterLast(delimiter = '.'),
val isNested: Boolean = false,
val topLevelClass: String = ""
) : ICompletionData {
val nameWithoutTopLevel: String
get() {
if (!isNested) {
return className
}
return className.substring(topLevelClass.length + 1)
}
val nameWithoutTopLevel: String = if (isNested) {
className.substring(topLevelClass.length + 1)
} else {
className
}
}
/**
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.projects
package com.itsaky.androidide.projects.internal
import androidx.annotation.RestrictTo
import com.android.builder.model.v2.models.ProjectSyncIssues
@@ -29,6 +29,9 @@ import com.itsaky.androidide.eventbus.events.file.FileEvent
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.CachingProject
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.R
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.projects.api.ModuleProject
import com.itsaky.androidide.projects.api.Project
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.projects
package com.itsaky.androidide.projects.internal
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.projects.api.GradleProject