From 0c1b03d646c2c6a4d74f084077c61ea42fbcd7a1 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sun, 28 Aug 2022 23:53:06 +0530 Subject: [PATCH] Complete attribute values in manifest files --- .../androidide/projects/api/AndroidModule.kt | 28 ++++ .../xml/resources/ResourceTableRegistry.kt | 15 ++ .../internal/DefaultResourceTableRegistry.kt | 102 ++++++++++- .../resources/ResourceTableRegistryTest.kt | 88 +++++++++- .../xml/providers/XmlCompletionProvider.kt | 2 + .../common/AttrValueCompletionProvider.kt | 32 +++- .../layout/LayoutTagCompletionProvider.kt | 10 -- .../layout/QualifiedTagCompleter.kt | 1 + .../ManifestAttrCompletionProvider.kt | 9 +- .../ManifestAttrValueCompletionProvider.kt | 140 ++++++++++++++++ .../manifest/manifestFileCompletions.kt | 9 + .../providers/completion/xmlCompletions.kt | 33 ++++ ...ManifestAttrValueCompletionProviderTest.kt | 158 ++++++++++++++++++ tests/test-home/.androidide/init/model.jar | Bin 59879 -> 59879 bytes ...tActivityActionCompletionTest_template.xml | 21 +++ .../ManifestAttrValueTest_template.xml | 22 +++ ...anifestCategoryCompletionTest_template.xml | 21 +++ ...ManifestFeatureCompletionTest_template.xml | 23 +++ ...tReceiverActionCompletionTest_template.xml | 21 +++ ...stServiceActionCompletionTest_template.xml | 21 +++ 20 files changed, 720 insertions(+), 36 deletions(-) create mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrValueCompletionProvider.kt create mode 100644 lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt create mode 100644 lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestAttrValueCompletionProviderTest.kt create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestActivityActionCompletionTest_template.xml create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestAttrValueTest_template.xml create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestCategoryCompletionTest_template.xml create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestFeatureCompletionTest_template.xml create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestReceiverActionCompletionTest_template.xml create mode 100644 tests/test-project/app/src/main/resources/completion/ManifestServiceActionCompletionTest_template.xml 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 31e338cfc..207b7d77f 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 @@ -358,5 +358,33 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this.. return ResourceTableRegistry.getInstance().getManifestAttrTable(platform) } + /** @see ResourceTableRegistry.getActivityActions */ + fun getActivityActions(): List { + return ResourceTableRegistry.getInstance() + .getActivityActions(getPlatformDir() ?: return emptyList()) + } + + /** @see ResourceTableRegistry.getBroadcastActions */ + fun getBroadcastActions(): List { + return ResourceTableRegistry.getInstance() + .getBroadcastActions(getPlatformDir() ?: return emptyList()) + } + + /** @see ResourceTableRegistry.getServiceActions */ + fun getServiceActions(): List { + return ResourceTableRegistry.getInstance() + .getServiceActions(getPlatformDir() ?: return emptyList()) + } + + /** @see ResourceTableRegistry.getCategories */ + fun getCategories(): List { + return ResourceTableRegistry.getInstance().getCategories(getPlatformDir() ?: return emptyList()) + } + + /** @see ResourceTableRegistry.getFeatures */ + fun getFeatures(): List { + return ResourceTableRegistry.getInstance().getFeatures(getPlatformDir() ?: return emptyList()) + } + private fun getPlatformDir() = bootClassPaths.firstOrNull { it.name == "android.jar" }?.parentFile } diff --git a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt index c31903ffd..f90fbf92f 100644 --- a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt +++ b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/ResourceTableRegistry.kt @@ -62,6 +62,21 @@ interface ResourceTableRegistry : XmlRegistry { */ fun getManifestAttrTable(platform: File): ResourceTable? + /** Get the list of all activity actions for the given [platform] directory. */ + fun getActivityActions(platform: File): List + + /** Get the list of all broadcast actions for the given [platform] directory. */ + fun getBroadcastActions(platform: File): List + + /** Get the list of all service actions for the given [platform] directory. */ + fun getServiceActions(platform: File): List + + /** Get the list of all categories for the given [platform] directory. */ + fun getCategories(platform: File): List + + /** Get the list of all features for the given [platform] directory. */ + fun getFeatures(platform: File): List + override fun forPlatformDir(platform: File): ResourceTable? { return forPackage(PCK_ANDROID, File(platform, "data/res")) } diff --git a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/internal/DefaultResourceTableRegistry.kt b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/internal/DefaultResourceTableRegistry.kt index c7de0e541..8344cd061 100644 --- a/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/internal/DefaultResourceTableRegistry.kt +++ b/build-tools/xml-utils/src/main/java/com/itsaky/androidide/xml/resources/internal/DefaultResourceTableRegistry.kt @@ -17,6 +17,11 @@ package com.itsaky.androidide.xml.resources.internal +import com.android.SdkConstants +import com.android.SdkConstants.FN_INTENT_ACTIONS_ACTIVITY +import com.android.SdkConstants.FN_INTENT_ACTIONS_BROADCAST +import com.android.SdkConstants.FN_INTENT_ACTIONS_SERVICE +import com.android.SdkConstants.FN_INTENT_CATEGORIES import com.android.SdkConstants.OS_PLATFORM_ATTRS_MANIFEST_XML import com.android.aaptcompiler.BlameLogger import com.android.aaptcompiler.ResourceTable @@ -28,6 +33,11 @@ import com.itsaky.androidide.layoutlib.resources.ResourceVisibility.PUBLIC import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.xml.resources.ResourceTableRegistry import com.itsaky.androidide.xml.resources.ResourceTableRegistry.Companion.PCK_ANDROID +import com.itsaky.androidide.xml.resources.internal.DefaultResourceTableRegistry.SingleLineValueEntryType.ACTIVITY_ACTIONS +import com.itsaky.androidide.xml.resources.internal.DefaultResourceTableRegistry.SingleLineValueEntryType.BROADCAST_ACTIONS +import com.itsaky.androidide.xml.resources.internal.DefaultResourceTableRegistry.SingleLineValueEntryType.CATEGORIES +import com.itsaky.androidide.xml.resources.internal.DefaultResourceTableRegistry.SingleLineValueEntryType.FEATURES +import com.itsaky.androidide.xml.resources.internal.DefaultResourceTableRegistry.SingleLineValueEntryType.SERVICE_ACTIONS import java.io.File import java.util.concurrent.ConcurrentHashMap @@ -38,11 +48,26 @@ import java.util.concurrent.ConcurrentHashMap */ internal object DefaultResourceTableRegistry : ResourceTableRegistry { + /** + * Represents the type of single line entries read from files. + * + * @property filename The filename which contains the single-line entries. + */ + internal enum class SingleLineValueEntryType(val filename: String) { + ACTIVITY_ACTIONS(FN_INTENT_ACTIONS_ACTIVITY), + BROADCAST_ACTIONS(FN_INTENT_ACTIONS_BROADCAST), + SERVICE_ACTIONS(FN_INTENT_ACTIONS_SERVICE), + CATEGORIES(FN_INTENT_CATEGORIES), + FEATURES("features.txt") + } + private val log = ILogger.newInstance(javaClass.simpleName) private val tables = ConcurrentHashMap() private val platformTables = ConcurrentHashMap() private val manifestAttrs = ConcurrentHashMap() + private val singleLineValueEntries = + ConcurrentHashMap>>() override fun forPackage(name: String, vararg dirs: File): ResourceTable? { @@ -59,6 +84,11 @@ internal object DefaultResourceTableRegistry : ResourceTableRegistry { override fun forPlatformDir(platform: File): ResourceTable? { getManifestAttrTable(platform) + getActivityActions(platform) + getBroadcastActions(platform) + getServiceActions(platform) + getCategories(platform) + getFeatures(platform) return super.forPlatformDir(platform) } @@ -70,6 +100,70 @@ internal object DefaultResourceTableRegistry : ResourceTableRegistry { } } + override fun getActivityActions(platform: File): List { + return getSingleLineEntry(platform, ACTIVITY_ACTIONS) + } + + override fun getBroadcastActions(platform: File): List { + return getSingleLineEntry(platform, BROADCAST_ACTIONS) + } + + override fun getServiceActions(platform: File): List { + return getSingleLineEntry(platform, SERVICE_ACTIONS) + } + + override fun getCategories(platform: File): List { + return getSingleLineEntry(platform, CATEGORIES) + } + + override fun getFeatures(platform: File): List { + return getSingleLineEntry(platform, FEATURES) + } + + override fun removeTable(packageName: String) { + tables.remove(packageName) + } + + override fun clear() { + tables.clear() + } + + private fun getSingleLineEntry(platform: File, type: SingleLineValueEntryType): List { + var entries = singleLineValueEntries[platform.path] + if (entries == null) { + entries = readSingleLineEntry(platform, type) + singleLineValueEntries[platform.path] = entries + } + + return entries[type] + ?: run { + readSingleLineEntriesTo(platform, type, entries) + entries[type] ?: emptyList() + } + } + + private fun readSingleLineEntry( + platform: File, + type: SingleLineValueEntryType + ): ConcurrentHashMap> { + val map = ConcurrentHashMap>() + readSingleLineEntriesTo(platform, type, map) + return map + } + + private fun readSingleLineEntriesTo( + platform: File, + type: SingleLineValueEntryType, + map: ConcurrentHashMap> + ) { + val file = File(platform, "${SdkConstants.FD_DATA}/${type.filename}") + if (!file.exists() || !file.canRead()) { + return + } + + map[type] = file.readLines() + } + private fun createManifestAttrTable(platform: File): ResourceTable? { val attrs = File(platform, OS_PLATFORM_ATTRS_MANIFEST_XML) if (!attrs.exists()) { @@ -120,10 +214,6 @@ internal object DefaultResourceTableRegistry : ResourceTableRegistry { return options } - override fun removeTable(packageName: String) { - tables.remove(packageName) - } - private fun updateFromDirectory( directory: File, table: ResourceTable, @@ -184,8 +274,4 @@ internal object DefaultResourceTableRegistry : ResourceTableRegistry { } } } - - override fun clear() { - tables.clear() - } } diff --git a/build-tools/xml-utils/src/test/java/com/itsaky/androidide/xml/resources/ResourceTableRegistryTest.kt b/build-tools/xml-utils/src/test/java/com/itsaky/androidide/xml/resources/ResourceTableRegistryTest.kt index 9154f9ccb..89ab660a0 100644 --- a/build-tools/xml-utils/src/test/java/com/itsaky/androidide/xml/resources/ResourceTableRegistryTest.kt +++ b/build-tools/xml-utils/src/test/java/com/itsaky/androidide/xml/resources/ResourceTableRegistryTest.kt @@ -29,10 +29,10 @@ import com.google.common.truth.Truth.assertThat import com.itsaky.androidide.xml.findAndroidJar import com.itsaky.androidide.xml.resources.ResourceTableRegistry.Companion.PCK_ANDROID import com.itsaky.androidide.xml.resources.internal.DefaultResourceTableRegistry +import java.io.File import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import java.io.File /** @author Akash Yadav */ @RunWith(RobolectricTestRunner::class) @@ -47,6 +47,7 @@ class ResourceTableRegistryTest { @Test fun `test with simple framework resource parsing`() { val androidJar = findAndroidJar() + val platform = androidJar.parentFile!! val registry = ResourceTableRegistry.getInstance() val resDir = File(androidJar.parentFile, "data/res") val resourceTable = registry.forPackage(PCK_ANDROID, resDir) @@ -57,7 +58,7 @@ class ResourceTableRegistryTest { assertThat(registry.forPackage(PCK_ANDROID, resDir)).isEqualTo(resourceTable) // It should also create the manifest attrs table by default - registry.getManifestAttrTable(platform = androidJar.parentFile!!).apply { + registry.getManifestAttrTable(platform = platform).apply { assertThat(this).isNotNull() assertThat(this!!.packages).hasSize(1) assertThat(this.packages.first().name).isEqualTo(PCK_ANDROID) @@ -65,6 +66,89 @@ class ResourceTableRegistryTest { assertThat(findPackage(PCK_ANDROID)!!.findGroup(STYLEABLE)).isNotNull() } + registry.getActivityActions(platform).apply { + assertThat(this).isNotEmpty() + assertThat(this) + .containsAtLeastElementsIn( + arrayOf( + "android.app.action.SET_NEW_PASSWORD", + "android.app.action.START_ENCRYPTION", + "android.intent.action.CARRIER_SETUP", + "android.intent.action.CHOOSER", + "android.intent.action.MAIN", + "android.intent.action.MANAGE_APP_PERMISSION", + "android.intent.action.MANAGE_APP_PERMISSIONS" + ) + ) + assertThat(this.size).isGreaterThan(250) // actual size is 300+ + } + + registry.getBroadcastActions(platform).apply { + assertThat(this).isNotEmpty() + assertThat(this) + .containsAtLeastElementsIn( + arrayOf( + "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED", + "android.telephony.action.SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED", + "android.telephony.euicc.action.NOTIFY_CARRIER_SETUP_INCOMPLETE", + "android.telephony.euicc.action.OTA_STATUS_CHANGED", + "android.app.action.USER_ADDED", + "android.app.action.USER_REMOVED" + ) + ) + } + + registry.getServiceActions(platform).apply { + assertThat(this).isNotEmpty() + assertThat(this) + .containsAtLeastElementsIn( + arrayOf( + "android.telephony.data.DataService", + "android.view.InputMethod", + "android.intent.action.TTS_SERVICE", + "android.media.MediaRoute2ProviderService", + "android.service.voice.VoiceInteractionService", + "android.service.vr.VrListenerService" + ) + ) + } + + registry.getCategories(platform).apply { + assertThat(this).isNotEmpty() + assertThat(this) + .containsAtLeastElementsIn( + arrayOf( + "android.intent.category.HOME", + "android.intent.category.HOME_MAIN", + "android.intent.category.INFO", + "android.intent.category.LAUNCHER", + "android.intent.category.LAUNCHER_APP", + "android.net.category.EVENT_IKE_ERROR", + "android.net.category.EVENT_NETWORK_ERROR", + "android.service.quicksettings.action.QS_TILE_PREFERENCES" + ) + ) + } + + registry.getFeatures(platform).apply { + assertThat(this).isNotEmpty() + assertThat(this) + .containsAtLeastElementsIn( + arrayOf( + "android.software.vulkan.deqp.level", + "android.software.webview", + "android.software.window_magnification", + "android.sofware.nfc.beam", + "android.hardware.strongbox_keystore", + "android.hardware.telephony", + "android.hardware.telephony.calling", + "android.hardware.telephony.carrierlock", + "android.hardware.ethernet", + "android.hardware.faketouch" + ) + ) + } + resourceTable.apply { assertThat(this).isNotNull() assertThat(this!!.packages).hasSize(1) diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt index e7a8cd365..b8ec9ea17 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/XmlCompletionProvider.kt @@ -32,6 +32,7 @@ import com.itsaky.androidide.lsp.xml.providers.completion.common.AttrValueComple import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutAttrCompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.layout.LayoutTagCompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestAttrCompletionProvider +import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestAttrValueCompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.manifest.ManifestTagCompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.manifest.canCompleteManifest import com.itsaky.androidide.lsp.xml.utils.XmlUtils @@ -149,6 +150,7 @@ class XmlCompletionProvider(settings: IServerSettings) : return when (type) { TAG -> ManifestTagCompletionProvider(this) ATTRIBUTE -> ManifestAttrCompletionProvider(this) + ATTRIBUTE_VALUE -> ManifestAttrValueCompletionProvider(this) else -> null } } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/AttrValueCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/AttrValueCompletionProvider.kt index aa04bb3e7..8c989b626 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/AttrValueCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/common/AttrValueCompletionProvider.kt @@ -45,14 +45,20 @@ import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH import com.itsaky.androidide.lsp.xml.providers.completion.IXmlCompletionProvider import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType.ATTRIBUTE_VALUE +import org.eclipse.lemminx.dom.DOMAttr import org.eclipse.lemminx.dom.DOMDocument +import org.eclipse.lemminx.dom.DOMNode /** * Provides completions for attribute value in layout XML files. * * @author Akash Yadav */ -class AttrValueCompletionProvider(provider: ICompletionProvider) : IXmlCompletionProvider(provider) { +open class AttrValueCompletionProvider(provider: ICompletionProvider) : + IXmlCompletionProvider(provider) { + + protected lateinit var nodeAtCursor: DOMNode + protected lateinit var attrAtCursor: DOMAttr override fun canProvideCompletions(pathData: ResourcePathData, type: NodeType): Boolean { return super.canProvideCompletions(pathData, type) && type == ATTRIBUTE_VALUE @@ -65,9 +71,10 @@ class AttrValueCompletionProvider(provider: ICompletionProvider) : IXmlCompletio type: NodeType, prefix: String ): CompletionResult { - val attr = document.findAttrAt(params.position.requireIndex()) + this.nodeAtCursor = document.findNodeAt(params.position.requireIndex()) ?: return EMPTY + this.attrAtCursor = document.findAttrAt(params.position.requireIndex()) ?: return EMPTY val attrName = - attr?.localName + attrAtCursor.localName ?: run { log.warn("Cannot find attribute at index ${params.position.index}") return EMPTY @@ -78,9 +85,9 @@ class AttrValueCompletionProvider(provider: ICompletionProvider) : IXmlCompletio // not for 'textColor="@@cursor"' val namespace = - attr.namespaceURI + attrAtCursor.namespaceURI ?: run { - log.warn("Unknown namespace for attribute", attr) + log.warn("Unknown namespace for attribute", attrAtCursor) return EMPTY } @@ -106,6 +113,17 @@ class AttrValueCompletionProvider(provider: ICompletionProvider) : IXmlCompletio } } + completeInternal(attrName, prefix, groups, list) + + return CompletionResult(list) + } + + protected open fun completeInternal( + attrName: String, + prefix: String, + groups: MutableSet>, + result: MutableList + ) { for (triple in groups) { val pack = triple.first val table = triple.second @@ -115,10 +133,8 @@ class AttrValueCompletionProvider(provider: ICompletionProvider) : IXmlCompletio continue } - addFromTable(table, entry, pack, prefix, list) + addFromTable(table, entry, pack, prefix, result) } - - return CompletionResult(list) } private fun addFromTable( diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt index 169dd7cdd..4e050358c 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/LayoutTagCompletionProvider.kt @@ -65,14 +65,4 @@ open class LayoutTagCompletionProvider(val provider: ICompletionProvider) : return SimpleTagCompleter(provider) } - - protected fun match(simpleName: String, qualifiedName: String, prefix: String): MatchLevel { - val simpleNameMatchLevel = matchLevel(simpleName, prefix) - val nameMatchLevel = matchLevel(qualifiedName, prefix) - if (simpleNameMatchLevel == NO_MATCH && nameMatchLevel == NO_MATCH) { - return NO_MATCH - } - - return MatchLevel.values()[max(simpleNameMatchLevel.ordinal, nameMatchLevel.ordinal)] - } } diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/QualifiedTagCompleter.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/QualifiedTagCompleter.kt index 0852dc7ac..1027aaa6c 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/QualifiedTagCompleter.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/layout/QualifiedTagCompleter.kt @@ -23,6 +23,7 @@ import com.itsaky.androidide.lsp.api.ICompletionProvider import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.xml.providers.completion.match import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType import com.itsaky.androidide.projects.api.ModuleProject import com.itsaky.androidide.utils.ClassTrie diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt index 0ad9ade30..c5b0e6a0a 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrCompletionProvider.kt @@ -20,13 +20,10 @@ package com.itsaky.androidide.lsp.xml.providers.completion.manifest import com.android.aaptcompiler.ConfigDescription import com.android.aaptcompiler.ResourceGroup import com.android.aaptcompiler.ResourcePathData -import com.android.aaptcompiler.ResourceTable import com.android.aaptcompiler.Styleable -import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.api.ICompletionProvider import com.itsaky.androidide.lsp.xml.providers.completion.AttrCompletionProvider import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType -import com.itsaky.androidide.xml.resources.ResourceTableRegistry import org.eclipse.lemminx.dom.DOMNode /** @@ -41,11 +38,7 @@ class ManifestAttrCompletionProvider(provider: ICompletionProvider) : return super.canProvideCompletions(pathData, type) && canCompleteManifest(pathData, type) } - override fun findResourceTables(nsUri: String): Set { - return setOf( - Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES) ?: return emptySet() - ) - } + override fun findResourceTables(nsUri: String) = manifestResourceTable() override fun findNodeStyleables(node: DOMNode, styleables: ResourceGroup): Set { val name = node.nodeName diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrValueCompletionProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrValueCompletionProvider.kt new file mode 100644 index 000000000..20bb8167f --- /dev/null +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/ManifestAttrValueCompletionProvider.kt @@ -0,0 +1,140 @@ +/* + * 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.lsp.xml.providers.completion.manifest + +import com.android.SdkConstants.ANDROID_NS_NAME_PREFIX +import com.android.SdkConstants.ATTR_NAME +import com.android.SdkConstants.TAG_ACTIVITY +import com.android.SdkConstants.TAG_INTENT_FILTER +import com.android.SdkConstants.TAG_RECEIVER +import com.android.SdkConstants.TAG_SERVICE +import com.android.aaptcompiler.ResourceGroup +import com.android.aaptcompiler.ResourceTable +import com.itsaky.androidide.lookup.Lookup +import com.itsaky.androidide.lsp.api.ICompletionProvider +import com.itsaky.androidide.lsp.models.CompletionItem +import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH +import com.itsaky.androidide.lsp.xml.providers.completion.common.AttrValueCompletionProvider +import com.itsaky.androidide.lsp.xml.providers.completion.match +import com.itsaky.androidide.projects.api.AndroidModule +import com.itsaky.androidide.projects.api.ModuleProject +import com.itsaky.androidide.xml.resources.ResourceTableRegistry + +/** + * Completes attribute values in manifest file. + * + * @author Akash Yadav + */ +class ManifestAttrValueCompletionProvider(provider: ICompletionProvider) : + AttrValueCompletionProvider(provider) { + + override fun completeInternal( + attrName: String, + prefix: String, + groups: MutableSet>, + result: MutableList + ) { + if (this.attrAtCursor.nodeName == /*android:name*/ "${ANDROID_NS_NAME_PREFIX}${ATTR_NAME}") { + when (this.nodeAtCursor.nodeName) { + "action" -> completeActionName(prefix, result) + "category" -> completeCategory(prefix, result) + // "uses-permission" -> completePermission(prefix, result) + "uses-feature" -> completeFeature(prefix, result) + } + } + super.completeInternal(attrName, prefix, groups, result) + } + + private fun completeActionName(prefix: String, result: MutableList) { + val parent = this.nodeAtCursor.parentNode ?: return + val parentOfParent = parent.parentNode ?: return + + if (parent.nodeName == TAG_INTENT_FILTER) { + when (parentOfParent.nodeName) { + TAG_ACTIVITY -> completeActivityActions(prefix, result) + TAG_RECEIVER -> completeReceiverActions(prefix, result) + TAG_SERVICE -> completeServiceActions(prefix, result) + } + } + } + + private fun completeServiceActions(prefix: String, result: MutableList) { + val module = getModule() + if (module is AndroidModule) { + addMatches(prefix, module.getServiceActions(), result) + } + } + + private fun completeActivityActions(prefix: String, result: MutableList) { + val module = getModule() + if (module is AndroidModule) { + addMatches(prefix, module.getActivityActions(), result) + } + } + + private fun completeReceiverActions(prefix: String, result: MutableList) { + val module = getModule() + if (module is AndroidModule) { + addMatches(prefix, module.getBroadcastActions(), result) + } + } + + private fun completeCategory(prefix: String, result: MutableList) { + val mod = getModule() + if (mod is AndroidModule) { + addMatches(prefix, mod.getCategories(), result) + } + } + + private fun completeFeature(prefix: String, result: MutableList) { + val mod = getModule() + if (mod is AndroidModule) { + addMatches(prefix, mod.getFeatures(), result) + } + } + + private fun addMatches( + prefix: String, + entries: List, + result: MutableList + ) { + for (entry in entries) { + val match = + if (entry.contains('.')) { + match(entry.substringAfterLast('.'), entry, prefix) + } else { + matchLevel(entry, prefix) + } + + if (match == NO_MATCH) { + continue + } + + result.add(createEnumOrFlagCompletionItem(ResourceTableRegistry.PCK_ANDROID, entry, match)) + } + } + + private fun getModule(): ModuleProject { + return Lookup.DEFAULT.lookup(ModuleProject.COMPLETION_MODULE_KEY) + ?: throw IllegalStateException("No module project provided") + } + + override fun findResourceTables(nsUri: String): Set { + return manifestResourceTable() + } +} diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/manifestFileCompletions.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/manifestFileCompletions.kt index 228fc2712..3e6113c0a 100644 --- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/manifestFileCompletions.kt +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/manifest/manifestFileCompletions.kt @@ -19,8 +19,11 @@ package com.itsaky.androidide.lsp.xml.providers.completion.manifest import com.android.SdkConstants.ANDROID_MANIFEST_XML import com.android.aaptcompiler.ResourcePathData +import com.android.aaptcompiler.ResourceTable +import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.xml.utils.XmlUtils.NodeType import com.itsaky.androidide.utils.VMUtils +import com.itsaky.androidide.xml.resources.ResourceTableRegistry const val MANIFEST_TAG_PREFIX = "AndroidManifest" @@ -31,6 +34,12 @@ fun canCompleteManifest(pathData: ResourcePathData, type: NodeType): Boolean { pathData.file.name.endsWith("_template.xml")) } +fun manifestResourceTable(): Set { + return setOf( + Lookup.DEFAULT.lookup(ResourceTableRegistry.COMPLETION_MANIFEST_ATTR_RES) ?: return emptySet() + ) +} + /** * Transforms entry name to tag name. * diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt new file mode 100644 index 000000000..0ca97580a --- /dev/null +++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/completion/xmlCompletions.kt @@ -0,0 +1,33 @@ +/* + * 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.lsp.xml.providers.completion + +import com.itsaky.androidide.lsp.models.CompletionItem.Companion.matchLevel +import com.itsaky.androidide.lsp.models.MatchLevel +import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH +import kotlin.math.min + +fun match(simpleName: String, qualifiedName: String, prefix: String): MatchLevel { + val simpleNameMatchLevel = matchLevel(simpleName, prefix) + val nameMatchLevel = matchLevel(qualifiedName, prefix) + if (simpleNameMatchLevel == NO_MATCH && nameMatchLevel == NO_MATCH) { + return NO_MATCH + } + + return MatchLevel.values()[min(simpleNameMatchLevel.ordinal, nameMatchLevel.ordinal)] +} diff --git a/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestAttrValueCompletionProviderTest.kt b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestAttrValueCompletionProviderTest.kt new file mode 100644 index 000000000..1eb55d4c8 --- /dev/null +++ b/lsp/xml/src/test/java/com/itsaky/androidide/lsp/xml/providers/completion/ManifestAttrValueCompletionProviderTest.kt @@ -0,0 +1,158 @@ +/* + * 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.lsp.xml.providers.completion + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.xml.CompletionHelper +import com.itsaky.androidide.lsp.xml.CompletionHelperImpl +import com.itsaky.androidide.lsp.xml.XMLLSPTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** @author Akash Yadav */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class ManifestAttrValueCompletionProviderTest : CompletionHelper by CompletionHelperImpl() { + + @Before fun setup() = XMLLSPTest.initProjectIfNeeded() + + @Test + fun `simple value completion test`() { + XMLLSPTest.apply { + openFile("completion/ManifestAttrValueTest") + val (incomplete, items) = complete() + assertThat(incomplete).isFalse() + assertThat(items) + .containsAtLeast( + "game", + "audio", + "video", + "image", + "social", + "news", + "maps", + "productivity", + "accessibility" + ) + } + } + + @Test // prefix: 'M' + fun `activity action value completion test`() { + XMLLSPTest.apply { + assertHasSingleLineEntries( + "ManifestActivityActionCompletionTest", + arrayOf( + "android.intent.action.MAIN", + "android.intent.action.MANAGE_APP_PERMISSION", + "android.intent.action.MANAGE_APP_PERMISSIONS" + ) + ) + } + } + + @Test // prefix: 'M' + fun `receiver action value completion test`() { + XMLLSPTest.apply { + assertHasSingleLineEntries( + "ManifestReceiverActionCompletionTest", + arrayOf( + "android.intent.action.MANAGE_PACKAGE_STORAGE", + "android.intent.action.MASTER_CLEAR_NOTIFICATION", + "android.intent.action.MEDIA_BAD_REMOVAL", + "android.intent.action.MEDIA_BUTTON", + "android.intent.action.MEDIA_CHECKING", + "android.intent.action.MEDIA_EJECT", + "android.intent.action.MEDIA_MOUNTED", + "android.intent.action.MEDIA_NOFS", + "android.intent.action.MEDIA_REMOVED", + "android.intent.action.MEDIA_SCANNER_FINISHED", + "android.intent.action.MEDIA_SCANNER_SCAN_FILE", + "android.intent.action.MEDIA_SCANNER_STARTED", + "android.intent.action.MEDIA_SHARED", + "android.intent.action.MEDIA_UNMOUNTABLE", + "android.intent.action.MEDIA_UNMOUNTED", + "android.intent.action.MY_PACKAGE_REPLACED", + "android.intent.action.MY_PACKAGE_SUSPENDED" + ) + ) + } + } + + @Test // prefix: 'M' + fun `service action value completion test`() { + XMLLSPTest.apply { + assertHasSingleLineEntries( + "ManifestServiceActionCompletionTest", + arrayOf( + "android.media.MediaRoute2ProviderService", + "android.media.browse.MediaBrowserService" + ) + ) + } + } + + @Test // prefix: 'A' + fun `category value completion test`() { + XMLLSPTest.apply { + assertHasSingleLineEntries( + "ManifestCategoryCompletionTest", + arrayOf( + "android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET", + "android.intent.category.ALTERNATIVE", + "android.intent.category.APP_BROWSER", + "android.intent.category.APP_CALCULATOR", + "android.intent.category.APP_CALENDAR", + "android.intent.category.APP_CONTACTS", + "android.intent.category.APP_EMAIL", + "android.intent.category.APP_FILES", + "android.intent.category.APP_GALLERY", + "android.intent.category.APP_MAPS", + "android.intent.category.APP_MARKET", + "android.intent.category.APP_MESSAGING", + "android.intent.category.APP_MUSIC" + ) + ) + } + } + + @Test // prefix: 'f' + fun `feature value completion test`() { + XMLLSPTest.apply { + assertHasSingleLineEntries( + "ManifestFeatureCompletionTest", + arrayOf( + "android.hardware.faketouch", + "android.hardware.fingerprint", + "android.software.file_based_encryption", + "android.software.freeform_window_management" + ) + ) + } + } + + private fun XMLLSPTest.assertHasSingleLineEntries(file: String, expect: Array) { + openFile("completion/$file") + val (incomplete, items) = complete() + assertThat(incomplete).isFalse() + assertThat(items).containsAtLeastElementsIn(expect) + } +} diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index 1ddd1f0d61088a165d8c3ac367c90f629940db6c..1822e8dfbb396030f9296d483ac1920591cffa37 100644 GIT binary patch delta 31 ncmaEUnfdu;W}X0VW)?061`ZAe*?k*%GVU + + + + + + + + + + + diff --git a/tests/test-project/app/src/main/resources/completion/ManifestAttrValueTest_template.xml b/tests/test-project/app/src/main/resources/completion/ManifestAttrValueTest_template.xml new file mode 100644 index 000000000..2119f172a --- /dev/null +++ b/tests/test-project/app/src/main/resources/completion/ManifestAttrValueTest_template.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/tests/test-project/app/src/main/resources/completion/ManifestCategoryCompletionTest_template.xml b/tests/test-project/app/src/main/resources/completion/ManifestCategoryCompletionTest_template.xml new file mode 100644 index 000000000..fa67d3fff --- /dev/null +++ b/tests/test-project/app/src/main/resources/completion/ManifestCategoryCompletionTest_template.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/tests/test-project/app/src/main/resources/completion/ManifestFeatureCompletionTest_template.xml b/tests/test-project/app/src/main/resources/completion/ManifestFeatureCompletionTest_template.xml new file mode 100644 index 000000000..48f7602f3 --- /dev/null +++ b/tests/test-project/app/src/main/resources/completion/ManifestFeatureCompletionTest_template.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/tests/test-project/app/src/main/resources/completion/ManifestReceiverActionCompletionTest_template.xml b/tests/test-project/app/src/main/resources/completion/ManifestReceiverActionCompletionTest_template.xml new file mode 100644 index 000000000..09e4e4255 --- /dev/null +++ b/tests/test-project/app/src/main/resources/completion/ManifestReceiverActionCompletionTest_template.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/tests/test-project/app/src/main/resources/completion/ManifestServiceActionCompletionTest_template.xml b/tests/test-project/app/src/main/resources/completion/ManifestServiceActionCompletionTest_template.xml new file mode 100644 index 000000000..f0cb56505 --- /dev/null +++ b/tests/test-project/app/src/main/resources/completion/ManifestServiceActionCompletionTest_template.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + +